diff --git a/Content.Client/Atmos/EntitySystems/GasCanisterAppearanceSystem.cs b/Content.Client/Atmos/EntitySystems/GasCanisterAppearanceSystem.cs
new file mode 100644
index 00000000000..e351b672bd0
--- /dev/null
+++ b/Content.Client/Atmos/EntitySystems/GasCanisterAppearanceSystem.cs
@@ -0,0 +1,31 @@
+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 class GasCanisterAppearanceSystem : VisualizerSystem
+{
+ [Dependency] private readonly 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 null)
+ 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);
+ }
+}
diff --git a/Content.Client/Atmos/UI/GasPressurePumpBoundUserInterface.cs b/Content.Client/Atmos/UI/GasPressurePumpBoundUserInterface.cs
index 0c07eec4025..b2bd92f6f1b 100644
--- a/Content.Client/Atmos/UI/GasPressurePumpBoundUserInterface.cs
+++ b/Content.Client/Atmos/UI/GasPressurePumpBoundUserInterface.cs
@@ -1,8 +1,6 @@
-using Content.Shared.Atmos;
-using Content.Shared.Atmos.Components;
+using Content.Shared.Atmos.Components;
using Content.Shared.Atmos.Piping.Binary.Components;
using Content.Shared.IdentityManagement;
-using Content.Shared.Localizations;
using JetBrains.Annotations;
using Robust.Client.UserInterface;
@@ -14,9 +12,6 @@ namespace Content.Client.Atmos.UI;
[UsedImplicitly]
public sealed class GasPressurePumpBoundUserInterface : BoundUserInterface
{
- [ViewVariables]
- private const float MaxPressure = Atmospherics.MaxOutputPressure;
-
[ViewVariables]
private GasPressurePumpWindow? _window;
diff --git a/Content.Client/Charges/ChargesSystem.cs b/Content.Client/Charges/ChargesSystem.cs
new file mode 100644
index 00000000000..d99a5763330
--- /dev/null
+++ b/Content.Client/Charges/ChargesSystem.cs
@@ -0,0 +1,52 @@
+using Content.Client.Actions;
+using Content.Shared.Actions;
+using Content.Shared.Charges.Components;
+using Content.Shared.Charges.Systems;
+
+namespace Content.Client.Charges;
+
+public sealed class ChargesSystem : SharedChargesSystem
+{
+ [Dependency] private readonly ActionsSystem _actions = default!;
+
+ private readonly Dictionary _lastCharges = new();
+ private readonly Dictionary _tempLastCharges = new();
+
+ public override void Update(float frameTime)
+ {
+ // Technically this should probably be in frameupdate but no one will ever notice a tick of delay on this.
+ base.Update(frameTime);
+
+ if (!_timing.IsFirstTimePredicted)
+ return;
+
+ // Update recharging actions. Server doesn't actually care about this and it's a waste of performance, actions are immediate.
+ var query = AllEntityQuery();
+
+ while (query.MoveNext(out var uid, out var recharge, out var charges))
+ {
+ BaseActionComponent? actionComp = null;
+
+ if (!_actions.ResolveActionData(uid, ref actionComp, logError: false))
+ continue;
+
+ var current = GetCurrentCharges((uid, charges, recharge));
+
+ if (!_lastCharges.TryGetValue(uid, out var last) || current != last)
+ {
+ _actions.UpdateAction(uid, actionComp);
+ }
+
+ _tempLastCharges[uid] = current;
+ }
+
+ _lastCharges.Clear();
+
+ foreach (var (uid, value) in _tempLastCharges)
+ {
+ _lastCharges[uid] = value;
+ }
+
+ _tempLastCharges.Clear();
+ }
+}
diff --git a/Content.Client/Charges/Systems/ChargesSystem.cs b/Content.Client/Charges/Systems/ChargesSystem.cs
deleted file mode 100644
index 9170ac5e948..00000000000
--- a/Content.Client/Charges/Systems/ChargesSystem.cs
+++ /dev/null
@@ -1,5 +0,0 @@
-using Content.Shared.Charges.Systems;
-
-namespace Content.Client.Charges.Systems;
-
-public sealed class ChargesSystem : SharedChargesSystem { }
diff --git a/Content.Client/Doors/DoorSystem.cs b/Content.Client/Doors/DoorSystem.cs
index 5e3de813d65..3d9a3e2a9aa 100644
--- a/Content.Client/Doors/DoorSystem.cs
+++ b/Content.Client/Doors/DoorSystem.cs
@@ -1,16 +1,18 @@
using Content.Shared.Doors.Components;
using Content.Shared.Doors.Systems;
+using Content.Shared.SprayPainter.Prototypes;
using Robust.Client.Animations;
using Robust.Client.GameObjects;
-using Robust.Client.ResourceManagement;
-using Robust.Shared.Serialization.TypeSerializers.Implementations;
+using Robust.Shared.Prototypes;
namespace Content.Client.Doors;
public sealed class DoorSystem : SharedDoorSystem
{
[Dependency] private readonly AnimationPlayerSystem _animationSystem = default!;
- [Dependency] private readonly IResourceCache _resourceCache = default!;
+ [Dependency] private readonly IComponentFactory _componentFactory = default!;
+ [Dependency] private readonly IPrototypeManager _prototypeManager = default!;
+ [Dependency] private readonly SpriteSystem _sprite = default!;
public override void Initialize()
{
@@ -84,8 +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(args.Sprite, baseRsi);
+ if (AppearanceSystem.TryGetData(entity, PaintableVisuals.Prototype, out var prototype, args.Component))
+ UpdateSpriteLayers((entity.Owner, args.Sprite), prototype);
if (_animationSystem.HasRunningAnimation(entity, DoorComponent.AnimationKey))
_animationSystem.Stop(entity.Owner, DoorComponent.AnimationKey);
@@ -95,21 +97,21 @@ private void OnAppearanceChange(Entity entity, ref AppearanceChan
private void UpdateAppearanceForDoorState(Entity entity, SpriteComponent sprite, DoorState state)
{
- sprite.DrawDepth = state is DoorState.Open ? entity.Comp.OpenDrawDepth : entity.Comp.ClosedDrawDepth;
+ _sprite.SetDrawDepth((entity.Owner, sprite), state is DoorState.Open ? entity.Comp.OpenDrawDepth : entity.Comp.ClosedDrawDepth);
switch (state)
{
case DoorState.Open:
foreach (var (layer, layerState) in entity.Comp.OpenSpriteStates)
{
- sprite.LayerSetState(layer, layerState);
+ _sprite.LayerSetRsiState((entity.Owner, sprite), layer, layerState);
}
return;
case DoorState.Closed:
foreach (var (layer, layerState) in entity.Comp.ClosedSpriteStates)
{
- sprite.LayerSetState(layer, layerState);
+ _sprite.LayerSetRsiState((entity.Owner, sprite), layer, layerState);
}
return;
@@ -138,14 +140,14 @@ private void UpdateAppearanceForDoorState(Entity entity, SpriteCo
}
}
- private void UpdateSpriteLayers(SpriteComponent sprite, string baseRsi)
+ private void UpdateSpriteLayers(Entity sprite, string targetProto)
{
- if (!_resourceCache.TryGetResource(SpriteSpecifierSerializer.TextureRoot / baseRsi, out var res))
- {
- Log.Error("Unable to load RSI '{0}'. Trace:\n{1}", baseRsi, Environment.StackTrace);
+ if (!_prototypeManager.TryIndex(targetProto, out var target))
+ return;
+
+ if (!target.TryGetComponent(out SpriteComponent? targetSprite, _componentFactory))
return;
- }
- sprite.BaseRSI = res.RSI;
+ _sprite.SetBaseRsi(sprite.AsNullable(), targetSprite.BaseRSI);
}
}
diff --git a/Content.Client/IconSmoothing/IconSmoothComponent.cs b/Content.Client/IconSmoothing/IconSmoothComponent.cs
index 4667a81d4a4..7da4ff29dce 100644
--- a/Content.Client/IconSmoothing/IconSmoothComponent.cs
+++ b/Content.Client/IconSmoothing/IconSmoothComponent.cs
@@ -2,6 +2,8 @@
using Robust.Client.Graphics;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
+// ReSharper disable InconsistentNaming
+
namespace Content.Client.IconSmoothing
{
///
diff --git a/Content.Client/SprayPainter/SprayPainterSystem.cs b/Content.Client/SprayPainter/SprayPainterSystem.cs
index 6a1d27e98b7..8f7d7f03622 100644
--- a/Content.Client/SprayPainter/SprayPainterSystem.cs
+++ b/Content.Client/SprayPainter/SprayPainterSystem.cs
@@ -1,56 +1,129 @@
+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 class SprayPainterSystem : SharedSprayPainterSystem
{
- [Dependency] private readonly IResourceCache _resourceCache = default!;
+ [Dependency] private readonly UserInterfaceSystem _ui = 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 Proto.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 (!Proto.TryIndex(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 Proto.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);
diff --git a/Content.Client/SprayPainter/UI/SprayPainterBoundUserInterface.cs b/Content.Client/SprayPainter/UI/SprayPainterBoundUserInterface.cs
index 7d6a6cf2a5a..701ec80bac8 100644
--- a/Content.Client/SprayPainter/UI/SprayPainterBoundUserInterface.cs
+++ b/Content.Client/SprayPainter/UI/SprayPainterBoundUserInterface.cs
@@ -1,42 +1,96 @@
+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;
+ }
+
+ 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);
+ }
- 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 OnDecalAngleChanged(int angle)
+ {
+ SendPredictedMessage(new SprayPainterSetDecalAngleMessage(angle));
+ }
+
+ 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(ItemList.ItemListSelectedEventArgs args)
+ private void OnSpritePicked(string group, string style)
{
- SendMessage(new SprayPainterSpritePickedMessage(args.ItemIndex));
+ SendPredictedMessage(new SprayPainterSetPaintableStyleMessage(group, style));
}
- private void OnColorPicked(ItemList.ItemListSelectedEventArgs args)
+ private void OnSetPipeColor(ItemList.ItemListSelectedEventArgs args)
{
var key = _window?.IndexToColorKey(args.ItemIndex);
- SendMessage(new SprayPainterColorPickedMessage(key));
+ SendPredictedMessage(new SprayPainterSetPipeColorMessage(key));
}
}
diff --git a/Content.Client/SprayPainter/UI/SprayPainterDecals.xaml b/Content.Client/SprayPainter/UI/SprayPainterDecals.xaml
new file mode 100644
index 00000000000..0d5c8e4f167
--- /dev/null
+++ b/Content.Client/SprayPainter/UI/SprayPainterDecals.xaml
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Content.Client/SprayPainter/UI/SprayPainterDecals.xaml.cs b/Content.Client/SprayPainter/UI/SprayPainterDecals.xaml.cs
new file mode 100644
index 00000000000..64d1f78d3ce
--- /dev/null
+++ b/Content.Client/SprayPainter/UI/SprayPainterDecals.xaml.cs
@@ -0,0 +1,174 @@
+using System.Numerics;
+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;
+
+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;
+
+ private SpriteSystem? _sprite;
+ private string _selectedDecal = string.Empty;
+ private List _decals = [];
+
+ public SprayPainterDecals()
+ {
+ RobustXamlLoader.Load(this);
+
+ AddAngleButton.OnButtonUp += _ => AngleSpinBox.Value += 90;
+ SubAngleButton.OnButtonUp += _ => AngleSpinBox.Value -= 90;
+ SetZeroAngleButton.OnButtonUp += _ => AngleSpinBox.Value = 0;
+ AngleSpinBox.ValueChanged += args => OnAngleChanged?.Invoke(args.Value);
+
+ UseCustomColorCheckBox.OnPressed += UseCustomColorCheckBoxOnOnPressed;
+ SnapToTileCheckBox.OnPressed += SnapToTileCheckBoxOnOnPressed;
+ ColorSelector.OnColorChanged += OnColorSelected;
+ }
+
+ 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;
+
+ 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;
+ }
+}
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..fe2f5a87af0
--- /dev/null
+++ b/Content.Client/SprayPainter/UI/SprayPainterGroup.xaml.cs
@@ -0,0 +1,66 @@
+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;
+ }
+}
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 e799775bc62..eb1218ad678 100644
--- a/Content.Client/SprayPainter/UI/SprayPainterWindow.xaml.cs
+++ b/Content.Client/SprayPainter/UI/SprayPainterWindow.xaml.cs
@@ -1,25 +1,54 @@
+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 readonly IEntitySystemManager _sysMan = default!;
+ [Dependency] private readonly 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;
+
+ // 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 +59,246 @@ 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;
}
- public void Populate(List entries, int selectedStyle, string? selectedColorKey, Dictionary palette)
+ private void OnStyleSelected(ListData data)
{
+ if (data is SpriteListData listData)
+ OnSpritePicked?.Invoke(listData.Group, listData.Style);
+ }
+
+ ///
+ /// 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))
{
- currentPalette = palette;
+ _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);
+
+ 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;
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++;
}
}
+ }
+
+ # region Setters
+ public void SetSelectedStyles(Dictionary selectedStyles)
+ {
+ foreach (var (group, style) in selectedStyles)
+ {
+ if (!_paintableControls.TryGetValue(group, out var control))
+ continue;
- // Disable event so we don't send a new event for pre-selectedStyle entry and end up in a loop
+ control.SelectItemByStyle(style);
+ }
+ }
- if (selectedColorKey != null)
+ 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;
}
+ }
+
+ public void SetSelectedTab(int tab)
+ {
+ Tabs.CurrentTab = int.Min(tab, Tabs.ChildCount - 1);
+ }
+
+ public void SetSelectedDecal(string decal)
+ {
+ if (_sprayPainterDecals != null)
+ _sprayPainterDecals.SetSelectedDecal(decal);
+ }
- SpriteList.OnItemSelected -= OnSpritePicked;
- SpriteList[selectedStyle].Selected = true;
- SpriteList.OnItemSelected += OnSpritePicked;
+ 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);
+ }
+ # endregion
}
+
+public record SpriteListData(string Group, string Style, EntProtoId Prototype, int SelectedIndex) : ListData;
diff --git a/Content.Client/Storage/Visualizers/EntityStorageVisualizerSystem.cs b/Content.Client/Storage/Visualizers/EntityStorageVisualizerSystem.cs
index ee4f2fdfd6c..c10a24ac323 100644
--- a/Content.Client/Storage/Visualizers/EntityStorageVisualizerSystem.cs
+++ b/Content.Client/Storage/Visualizers/EntityStorageVisualizerSystem.cs
@@ -1,10 +1,15 @@
+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
{
+ [Dependency] private readonly IPrototypeManager _prototypeManager = default!;
+ [Dependency] private readonly IComponentFactory _componentFactory = default!;
+
public override void Initialize()
{
base.Initialize();
@@ -23,51 +28,77 @@ 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;
+ if (AppearanceSystem.TryGetData(uid, PaintableVisuals.Prototype, out var prototype, args.Component))
+ {
+ if (_prototypeManager.TryIndex(prototype, out var proto))
+ {
+ if (proto.TryGetComponent(out SpriteComponent? sprite, _componentFactory))
+ {
+ SpriteSystem.SetBaseRsi((uid, args.Sprite), sprite.BaseRSI);
+ }
+ if (proto.TryGetComponent(out EntityStorageVisualsComponent? visuals, _componentFactory))
+ {
+ comp.StateBaseOpen = visuals.StateBaseOpen;
+ comp.StateBaseClosed = visuals.StateBaseClosed;
+ comp.StateDoorOpen = visuals.StateDoorOpen;
+ comp.StateDoorClosed = visuals.StateDoorClosed;
+ forceRedrawBase = true;
+ }
+ }
+ }
+
// 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);
}
}
}
diff --git a/Content.Client/UserInterface/Systems/Actions/ActionUIController.cs b/Content.Client/UserInterface/Systems/Actions/ActionUIController.cs
index c2ba35f3c58..9b7ecd0a9e8 100644
--- a/Content.Client/UserInterface/Systems/Actions/ActionUIController.cs
+++ b/Content.Client/UserInterface/Systems/Actions/ActionUIController.cs
@@ -11,6 +11,7 @@
using Content.Client.UserInterface.Systems.Actions.Widgets;
using Content.Client.UserInterface.Systems.Actions.Windows;
using Content.Client.UserInterface.Systems.Gameplay;
+using Content.Client.UserInterface.Systems.MenuBar.Widgets;
using Content.Shared.Actions;
using Content.Shared.Input;
using Robust.Client.GameObjects;
@@ -42,7 +43,6 @@ public sealed class ActionUIController : UIController, IOnStateChanged UIManager.GetActiveUIWidgetOrNull();
- private MenuButton? ActionButton => UIManager.GetActiveUIWidgetOrNull()?.ActionButton;
+ private MenuButton? ActionButton => UIManager.GetActiveUIWidgetOrNull()?.ActionButton;
public bool IsDragging => _menuDragHelper.IsDragging;
@@ -483,7 +483,7 @@ private void PopulateActions(IEnumerable<(EntityUid Id, BaseActionComponent Comp
continue;
}
- var button = new ActionButton(_entMan, _spriteSystem, this) {Locked = true};
+ var button = new ActionButton(EntityManager, _spriteSystem, this) {Locked = true};
button.ActionPressed += OnWindowActionPressed;
button.ActionUnpressed += OnWindowActionUnPressed;
button.ActionFocusExited += OnWindowActionFocusExisted;
diff --git a/Content.Client/_DV/Felinids/FelinidSystem.cs b/Content.Client/_DV/Felinids/FelinidSystem.cs
new file mode 100644
index 00000000000..d55a060f6e9
--- /dev/null
+++ b/Content.Client/_DV/Felinids/FelinidSystem.cs
@@ -0,0 +1,3 @@
+namespace Content.Shared._DV.Abilities.Felinid;
+
+public sealed class FelinidSystem : SharedFelinidSystem;
diff --git a/Content.Server/Atmos/EntitySystems/AtmosPipeLayersSystem.cs b/Content.Server/Atmos/EntitySystems/AtmosPipeLayersSystem.cs
index c2c9050920e..7f431627270 100644
--- a/Content.Server/Atmos/EntitySystems/AtmosPipeLayersSystem.cs
+++ b/Content.Server/Atmos/EntitySystems/AtmosPipeLayersSystem.cs
@@ -44,7 +44,7 @@ public override void SetPipeLayer(Entity ent, AtmosPip
if (ent.Comp.PipeLayersLocked)
return;
- base.SetPipeLayer(ent, layer);
+ base.SetPipeLayer(ent, layer, user, used);
if (!TryComp(ent, out var nodeContainer))
return;
diff --git a/Content.Server/Atmos/IGasMixtureHolder.cs b/Content.Server/Atmos/IGasMixtureHolder.cs
deleted file mode 100644
index 65d7ba69a76..00000000000
--- a/Content.Server/Atmos/IGasMixtureHolder.cs
+++ /dev/null
@@ -1,9 +0,0 @@
-using Content.Shared.Atmos;
-
-namespace Content.Server.Atmos
-{
- public interface IGasMixtureHolder
- {
- public GasMixture Air { get; set; }
- }
-}
diff --git a/Content.Server/Atmos/Piping/Unary/EntitySystems/GasCanisterSystem.cs b/Content.Server/Atmos/Piping/Unary/EntitySystems/GasCanisterSystem.cs
index 292b6d94f82..f473207ca35 100644
--- a/Content.Server/Atmos/Piping/Unary/EntitySystems/GasCanisterSystem.cs
+++ b/Content.Server/Atmos/Piping/Unary/EntitySystems/GasCanisterSystem.cs
@@ -2,7 +2,6 @@
using Content.Server.Atmos.Components;
using Content.Server.Atmos.EntitySystems;
using Content.Server.Atmos.Piping.Components;
-using Content.Server.Atmos.Piping.Unary.Components;
using Content.Server.Cargo.Systems;
using Content.Server.NodeContainer;
using Content.Server.NodeContainer.EntitySystems;
@@ -11,6 +10,7 @@
using Content.Server.Popups;
using Content.Shared.Atmos;
using Content.Shared.Atmos.Piping.Binary.Components;
+using Content.Shared.Atmos.Piping.Unary.Components;
using Content.Shared.Containers.ItemSlots;
using Content.Shared.Database;
using Content.Shared.Interaction;
diff --git a/Content.Server/Charges/ChargesSystem.cs b/Content.Server/Charges/ChargesSystem.cs
new file mode 100644
index 00000000000..6883dcb03d0
--- /dev/null
+++ b/Content.Server/Charges/ChargesSystem.cs
@@ -0,0 +1,8 @@
+using Content.Shared.Charges.Systems;
+
+namespace Content.Server.Charges;
+
+public sealed class ChargesSystem : SharedChargesSystem
+{
+
+}
diff --git a/Content.Server/Charges/Components/AutoRechargeComponent.cs b/Content.Server/Charges/Components/AutoRechargeComponent.cs
deleted file mode 100644
index 165b181dcbc..00000000000
--- a/Content.Server/Charges/Components/AutoRechargeComponent.cs
+++ /dev/null
@@ -1,27 +0,0 @@
-using Content.Server.Charges.Systems;
-using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
-
-namespace Content.Server.Charges.Components;
-
-///
-/// Something with limited charges that can be recharged automatically.
-/// Requires LimitedChargesComponent to function.
-///
-// TODO: no reason this cant be predicted and server system deleted
-[RegisterComponent, AutoGenerateComponentPause]
-[Access(typeof(ChargesSystem))]
-public sealed partial class AutoRechargeComponent : Component
-{
- ///
- /// The time it takes to regain a single charge
- ///
- [DataField("rechargeDuration"), ViewVariables(VVAccess.ReadWrite)]
- public TimeSpan RechargeDuration = TimeSpan.FromSeconds(90);
-
- ///
- /// The time when the next charge will be added
- ///
- [DataField("nextChargeTime", customTypeSerializer: typeof(TimeOffsetSerializer))]
- [AutoPausedField]
- public TimeSpan NextChargeTime;
-}
diff --git a/Content.Server/Charges/Systems/ChargesSystem.cs b/Content.Server/Charges/Systems/ChargesSystem.cs
deleted file mode 100644
index 974928ee4bb..00000000000
--- a/Content.Server/Charges/Systems/ChargesSystem.cs
+++ /dev/null
@@ -1,53 +0,0 @@
-using Content.Server.Charges.Components;
-using Content.Shared.Charges.Components;
-using Content.Shared.Charges.Systems;
-using Content.Shared.Examine;
-using Robust.Shared.Timing;
-
-namespace Content.Server.Charges.Systems;
-
-public sealed class ChargesSystem : SharedChargesSystem
-{
- [Dependency] private readonly IGameTiming _timing = default!;
-
- public override void Update(float frameTime)
- {
- base.Update(frameTime);
-
- var query = EntityQueryEnumerator();
- while (query.MoveNext(out var uid, out var charges, out var recharge))
- {
- if (charges.Charges == charges.MaxCharges || _timing.CurTime < recharge.NextChargeTime)
- continue;
-
- AddCharges(uid, 1, charges);
- recharge.NextChargeTime = _timing.CurTime + recharge.RechargeDuration;
- }
- }
-
- protected override void OnExamine(EntityUid uid, LimitedChargesComponent comp, ExaminedEvent args)
- {
- base.OnExamine(uid, comp, args);
-
- // only show the recharging info if it's not full
- if (!args.IsInDetailsRange || comp.Charges == comp.MaxCharges || !TryComp(uid, out var recharge))
- return;
-
- var timeRemaining = Math.Round((recharge.NextChargeTime - _timing.CurTime).TotalSeconds);
- args.PushMarkup(Loc.GetString("limited-charges-recharging", ("seconds", timeRemaining)));
- }
-
- public override void AddCharges(EntityUid uid, int change, LimitedChargesComponent? comp = null)
- {
- if (!Query.Resolve(uid, ref comp, false))
- return;
-
- var startRecharge = comp.Charges == comp.MaxCharges;
- base.AddCharges(uid, change, comp);
-
- // if a charge was just used from full, start the recharge timer
- // TODO: probably make this an event instead of having le server system that just does this
- if (change < 0 && startRecharge && TryComp(uid, out var recharge))
- recharge.NextChargeTime = _timing.CurTime + recharge.RechargeDuration;
- }
-}
diff --git a/Content.Server/Flash/FlashSystem.cs b/Content.Server/Flash/FlashSystem.cs
index 1fb4c116f2e..079c90c4b3f 100644
--- a/Content.Server/Flash/FlashSystem.cs
+++ b/Content.Server/Flash/FlashSystem.cs
@@ -1,22 +1,22 @@
using System.Linq;
using Content.Server.Flash.Components;
-using Content.Shared.Flash.Components;
using Content.Server.Light.EntitySystems;
using Content.Server.Popups;
using Content.Server.Stunnable;
using Content.Shared._Goobstation.Flashbang;
using Content.Shared.Charges.Components;
using Content.Shared.Charges.Systems;
+using Content.Shared.Examine;
using Content.Shared.Eye.Blinding.Components;
using Content.Shared.Flash;
+using Content.Shared.Flash.Components;
using Content.Shared.IdentityManagement;
using Content.Shared.Interaction.Events;
using Content.Shared.Inventory;
+using Content.Shared.StatusEffect;
using Content.Shared.Tag;
using Content.Shared.Traits.Assorted;
using Content.Shared.Weapons.Melee.Events;
-using Content.Shared.StatusEffect;
-using Content.Shared.Examine;
using Robust.Server.Audio;
using Robust.Server.GameObjects;
using Robust.Shared.Audio;
@@ -29,7 +29,7 @@ internal sealed class FlashSystem : SharedFlashSystem
{
[Dependency] private readonly AppearanceSystem _appearance = default!;
[Dependency] private readonly AudioSystem _audio = default!;
- [Dependency] private readonly SharedChargesSystem _charges = default!;
+ [Dependency] private readonly SharedChargesSystem _sharedCharges = default!;
[Dependency] private readonly EntityLookupSystem _entityLookup = default!;
[Dependency] private readonly SharedTransformSystem _transform = default!;
[Dependency] private readonly ExamineSystemShared _examine = default!;
@@ -84,15 +84,15 @@ private bool UseFlash(EntityUid uid, FlashComponent comp, EntityUid user)
return false;
TryComp(uid, out var charges);
- if (_charges.IsEmpty(uid, charges))
+ if (_sharedCharges.IsEmpty((uid, charges)))
return false;
- _charges.UseCharge(uid, charges);
+ _sharedCharges.TryUseCharge((uid, charges));
_audio.PlayPvs(comp.Sound, uid);
comp.Flashing = true;
_appearance.SetData(uid, FlashVisuals.Flashing, true);
- if (_charges.IsEmpty(uid, charges))
+ if (_sharedCharges.IsEmpty((uid, charges)))
{
_appearance.SetData(uid, FlashVisuals.Burnt, true);
_tag.AddTag(uid, "Trash");
diff --git a/Content.Server/Mining/MiningSystem.cs b/Content.Server/Mining/MiningSystem.cs
index 569b49d6407..b5a1eda841c 100644
--- a/Content.Server/Mining/MiningSystem.cs
+++ b/Content.Server/Mining/MiningSystem.cs
@@ -1,3 +1,4 @@
+using Content.Shared._Null.Systems;
using Content.Shared.Destructible;
using Content.Shared.Mining;
using Content.Shared.Mining.Components;
@@ -11,7 +12,7 @@ namespace Content.Server.Mining;
///
/// This handles creating ores when the entity is destroyed.
///
-public sealed class MiningSystem : EntitySystem
+public sealed class MiningSystem : SharedMiningSystem
{
[Dependency] private readonly IPrototypeManager _proto = default!;
[Dependency] private readonly IRobustRandom _random = default!;
diff --git a/Content.Server/Nyanotrasen/Abilities/Felinid/CoughingUpHairballComponent.cs b/Content.Server/Nyanotrasen/Abilities/Felinid/CoughingUpHairballComponent.cs
deleted file mode 100644
index 1bfa0809b63..00000000000
--- a/Content.Server/Nyanotrasen/Abilities/Felinid/CoughingUpHairballComponent.cs
+++ /dev/null
@@ -1,11 +0,0 @@
-namespace Content.Server.Abilities.Felinid;
-
-[RegisterComponent]
-public sealed partial class CoughingUpHairballComponent : Component
-{
- [DataField("accumulator")]
- public float Accumulator = 0f;
-
- [DataField("coughUpTime")]
- public TimeSpan CoughUpTime = TimeSpan.FromSeconds(2.15); // length of hairball.ogg
-}
diff --git a/Content.Server/Nyanotrasen/Abilities/Felinid/FelinidComponent.cs b/Content.Server/Nyanotrasen/Abilities/Felinid/FelinidComponent.cs
deleted file mode 100644
index 9165b90de05..00000000000
--- a/Content.Server/Nyanotrasen/Abilities/Felinid/FelinidComponent.cs
+++ /dev/null
@@ -1,36 +0,0 @@
-using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
-using Robust.Shared.Prototypes;
-using Content.Shared.Actions;
-using Robust.Shared.Utility;
-
-namespace Content.Server.Abilities.Felinid;
-
-[RegisterComponent]
-public sealed partial class FelinidComponent : Component
-{
- ///
- /// The hairball prototype to use.
- ///
- [DataField("hairballPrototype", customTypeSerializer: typeof(PrototypeIdSerializer))]
- public string HairballPrototype = "Hairball";
-
- //[DataField("hairballAction", customTypeSerializer: typeof(PrototypeIdSerializer))]
- //public string HairballAction = "ActionHairball";
-
- [DataField("hairballActionId",
- customTypeSerializer: typeof(PrototypeIdSerializer))]
- public string? HairballActionId = "ActionHairball";
-
- [DataField("hairballAction")]
- public EntityUid? HairballAction;
-
- [DataField("eatActionId",
- customTypeSerializer: typeof(PrototypeIdSerializer))]
- public string? EatActionId = "ActionEatMouse";
-
- [DataField("eatAction")]
- public EntityUid? EatAction;
-
- [DataField("eatActionTarget")]
- public EntityUid? EatActionTarget = null;
-}
diff --git a/Content.Server/Nyanotrasen/Abilities/Felinid/FelinidFoodComponent.cs b/Content.Server/Nyanotrasen/Abilities/Felinid/FelinidFoodComponent.cs
deleted file mode 100644
index 93804eeb5a0..00000000000
--- a/Content.Server/Nyanotrasen/Abilities/Felinid/FelinidFoodComponent.cs
+++ /dev/null
@@ -1,5 +0,0 @@
-namespace Content.Server.Abilities.Felinid;
-
-[RegisterComponent]
-public sealed partial class FelinidFoodComponent : Component
-{ }
diff --git a/Content.Server/Nyanotrasen/Abilities/Felinid/FelinidSystem.cs b/Content.Server/Nyanotrasen/Abilities/Felinid/FelinidSystem.cs
deleted file mode 100644
index 74724f74bf6..00000000000
--- a/Content.Server/Nyanotrasen/Abilities/Felinid/FelinidSystem.cs
+++ /dev/null
@@ -1,202 +0,0 @@
-using Content.Shared.Actions;
-using Content.Shared.Actions.Events;
-using Content.Shared.Audio;
-using Content.Shared.StatusEffect;
-using Content.Shared.Throwing;
-using Content.Shared.Item;
-using Content.Shared.Inventory;
-using Content.Shared.Hands;
-using Content.Shared.IdentityManagement;
-using Content.Shared.Nutrition.Components;
-using Content.Shared.Nutrition.EntitySystems;
-using Content.Server.Body.Components;
-using Content.Server.Chemistry.Containers.EntitySystems;
-using Content.Server.Medical;
-using Content.Server.Nutrition.EntitySystems;
-using Content.Server.Nutrition.Components;
-using Content.Server.Chemistry.EntitySystems;
-using Content.Server.Popups;
-using Content.Shared.Chemistry.EntitySystems;
-using Robust.Shared.Audio;
-using Robust.Shared.Audio.Systems;
-using Robust.Shared.Player;
-using Robust.Shared.Random;
-using Robust.Shared.Prototypes;
-using Content.Shared.CombatMode.Pacification; // Frontier
-
-namespace Content.Server.Abilities.Felinid;
-
-public sealed partial class FelinidSystem : EntitySystem
-{
-
- [Dependency] private readonly SharedActionsSystem _actionsSystem = default!;
- [Dependency] private readonly HungerSystem _hungerSystem = default!;
- [Dependency] private readonly VomitSystem _vomitSystem = default!;
- [Dependency] private readonly SolutionContainerSystem _solutionSystem = default!;
- [Dependency] private readonly IRobustRandom _robustRandom = default!;
- [Dependency] private readonly PopupSystem _popupSystem = default!;
- [Dependency] private readonly InventorySystem _inventorySystem = default!;
- [Dependency] private readonly IPrototypeManager _prototypeManager = default!;
- [Dependency] private readonly SharedAudioSystem _audio = default!;
-
- public override void Initialize()
- {
- base.Initialize();
- SubscribeLocalEvent(OnInit);
- SubscribeLocalEvent(OnHairball);
- SubscribeLocalEvent(OnEatMouse);
- SubscribeLocalEvent(OnEquipped);
- SubscribeLocalEvent(OnUnequipped);
- SubscribeLocalEvent(OnHairballHit);
- SubscribeLocalEvent(OnHairballPickupAttempt);
- SubscribeLocalEvent(OnHairballAttemptPacifiedThrow); // Frontier - Block hairball abuse
- }
-
- private Queue RemQueue = new();
-
- public override void Update(float frameTime)
- {
- base.Update(frameTime);
- foreach (var cat in RemQueue)
- {
- RemComp(cat);
- }
- RemQueue.Clear();
-
- foreach (var (hairballComp, catComp) in EntityQuery())
- {
- hairballComp.Accumulator += frameTime;
- if (hairballComp.Accumulator < hairballComp.CoughUpTime.TotalSeconds)
- continue;
-
- hairballComp.Accumulator = 0;
- SpawnHairball(hairballComp.Owner, catComp);
- RemQueue.Enqueue(hairballComp.Owner);
- }
- }
-
- private void OnInit(EntityUid uid, FelinidComponent component, ComponentInit args)
- {
- if (component.HairballAction != null)
- return;
-
- //component.HairballAction = Spawn("ActionHairball");
- _actionsSystem.AddAction(uid, ref component.HairballAction, component.HairballActionId);
- }
-
- private void OnEquipped(EntityUid uid, FelinidComponent component, DidEquipHandEvent args)
- {
- if (!HasComp(args.Equipped))
- return;
-
- component.EatActionTarget = args.Equipped;
-
- //component.EatAction = Spawn("ActionEatMouse");
- _actionsSystem.AddAction(uid, ref component.EatAction, component.EatActionId);
- }
-
- private void OnUnequipped(EntityUid uid, FelinidComponent component, DidUnequipHandEvent args)
- {
- if (args.Unequipped == component.EatActionTarget)
- {
- component.EatActionTarget = null;
- if (component.EatAction != null)
- _actionsSystem.RemoveAction(uid, component.EatAction.Value);
- }
- }
-
- private void OnHairball(EntityUid uid, FelinidComponent component, HairballActionEvent args)
- {
- if (_inventorySystem.TryGetSlotEntity(uid, "mask", out var maskUid) &&
- EntityManager.TryGetComponent(maskUid, out var blocker) &&
- blocker.Enabled)
- {
- _popupSystem.PopupEntity(Loc.GetString("hairball-mask", ("mask", maskUid)), uid, uid);
- return;
- }
-
- _popupSystem.PopupEntity(Loc.GetString("hairball-cough", ("name", Identity.Entity(uid, EntityManager))), uid);
- _audio.PlayPvs("/Audio/Nyanotrasen/Effects/Species/hairball.ogg", uid, AudioHelpers.WithVariation(0.15f));
-
- EnsureComp(uid);
- args.Handled = true;
- }
-
- private void OnEatMouse(EntityUid uid, FelinidComponent component, EatMouseActionEvent args)
- {
- if (component.EatActionTarget == null)
- return;
-
- if (!TryComp(uid, out var hunger))
- return;
-
- if (hunger.CurrentThreshold == Shared.Nutrition.Components.HungerThreshold.Overfed)
- {
- _popupSystem.PopupEntity(Loc.GetString("food-system-you-cannot-eat-any-more"), uid, uid, Shared.Popups.PopupType.SmallCaution);
- return;
- }
-
- if (_inventorySystem.TryGetSlotEntity(uid, "mask", out var maskUid) &&
- EntityManager.TryGetComponent(maskUid, out var blocker) &&
- blocker.Enabled)
- {
- _popupSystem.PopupEntity(Loc.GetString("hairball-mask", ("mask", maskUid)), uid, uid, Shared.Popups.PopupType.SmallCaution);
- return;
- }
-
- if (component.HairballAction != null)
- {
- _actionsSystem.SetCharges(component.HairballAction, 1); // You get the charge back and that's it. Tough.
- _actionsSystem.SetEnabled(component.HairballAction, true);
- }
- Del(component.EatActionTarget.Value);
- component.EatActionTarget = null;
-
- _audio.PlayPvs("/Audio/_DV/Items/eatfood.ogg", uid, AudioHelpers.WithVariation(0.15f));
-
- _hungerSystem.ModifyHunger(uid, 50f, hunger);
-
- if (component.EatAction != null)
- _actionsSystem.RemoveAction(uid, component.EatAction.Value);
- }
-
- private void SpawnHairball(EntityUid uid, FelinidComponent component)
- {
- var hairball = EntityManager.SpawnEntity(component.HairballPrototype, Transform(uid).Coordinates);
- var hairballComp = Comp(hairball);
-
- if (TryComp(uid, out var bloodstream) && bloodstream.ChemicalSolution.HasValue)
- {
- var temp = _solutionSystem.SplitSolution(bloodstream.ChemicalSolution.Value, 20);
-
- if (_solutionSystem.TryGetSolution(hairball, hairballComp.SolutionName, out var hairballSolution))
- {
- _solutionSystem.TryAddSolution(hairballSolution.Value, temp);
- }
- }
- }
- private void OnHairballHit(EntityUid uid, HairballComponent component, ThrowDoHitEvent args)
- {
- if (HasComp(args.Target) || !HasComp(args.Target))
- return;
- if (_robustRandom.Prob(0.2f))
- _vomitSystem.Vomit(args.Target);
- }
-
- private void OnHairballPickupAttempt(EntityUid uid, HairballComponent component, GettingPickedUpAttemptEvent args)
- {
- if (HasComp(args.User) || !HasComp(args.User))
- return;
-
- if (_robustRandom.Prob(0.2f))
- {
- _vomitSystem.Vomit(args.User);
- args.Cancel();
- }
- }
-
- private void OnHairballAttemptPacifiedThrow(Entity ent, ref AttemptPacifiedThrowEvent args) // Frontier - Block hairball abuse
- {
- args.Cancel("pacified-cannot-throw-hairball");
- }
-}
diff --git a/Content.Server/Nyanotrasen/Abilities/Felinid/HairballComponent.cs b/Content.Server/Nyanotrasen/Abilities/Felinid/HairballComponent.cs
deleted file mode 100644
index 01c01dbc2e5..00000000000
--- a/Content.Server/Nyanotrasen/Abilities/Felinid/HairballComponent.cs
+++ /dev/null
@@ -1,7 +0,0 @@
-namespace Content.Server.Abilities.Felinid;
-
-[RegisterComponent]
-public sealed partial class HairballComponent : Component
-{
- public string SolutionName = "hairball";
-}
diff --git a/Content.Server/SprayPainter/SprayPainterSystem.cs b/Content.Server/SprayPainter/SprayPainterSystem.cs
index 9f6da20fda1..322edf43af6 100644
--- a/Content.Server/SprayPainter/SprayPainterSystem.cs
+++ b/Content.Server/SprayPainter/SprayPainterSystem.cs
@@ -1,27 +1,134 @@
+using System.Numerics;
using Content.Server.Atmos.Piping.Components;
using Content.Server.Atmos.Piping.EntitySystems;
+using Content.Server.Charges;
+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;
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 class SprayPainterSystem : SharedSprayPainterSystem
{
[Dependency] private readonly AtmosPipeColorSystem _pipeColor = default!;
+ [Dependency] private readonly PopupSystem _popup = default!;
+ [Dependency] private readonly DecalSystem _decals = default!;
+ [Dependency] private readonly AudioSystem _audio = default!;
+ [Dependency] private readonly ChargesSystem _charges = default!;
+ [Dependency] private readonly TransformSystem _transform = 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.CanReach || args.Target != null)
+ 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.GetCurrentCharges((ent, charges)) < ent.Comp.DecalChargeCost)
+ {
+ _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 Vector2(-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: IsDecalRemovable);
+ 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.TryUseCharges((ent, charges), ent.Comp.DecalChargeCost);
+
+ AdminLogger.Add(LogType.CrayonDraw, LogImpact.Low, $"{EntityManager.ToPrettyString(args.User):user} painted a {ent.Comp.SelectedDecal}");
+ }
+
+ ///
+ /// 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 bool IsDecalRemovable(Decal decal)
+ {
+ if (!Proto.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 +136,17 @@ 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.TryUseCharges((ent, charges), ent.Comp.PipeChargeCost))
+ return;
+ Audio.PlayPvs(ent.Comp.SpraySound, ent);
_pipeColor.SetColor(target, color, args.Color);
args.Handled = true;
@@ -47,13 +157,28 @@ 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.GetCurrentCharges((args.Used, charges)) < painter.PipeChargeCost)
+ {
+ 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,
diff --git a/Content.Server/_DV/Abilities/Chitinid/ChitinidSystem.cs b/Content.Server/_DV/Abilities/Chitinid/ChitinidSystem.cs
index 34d8244d5c9..75a336cea90 100644
--- a/Content.Server/_DV/Abilities/Chitinid/ChitinidSystem.cs
+++ b/Content.Server/_DV/Abilities/Chitinid/ChitinidSystem.cs
@@ -2,6 +2,8 @@
using Content.Shared.Actions;
using Content.Shared.Actions.Events;
using Content.Shared.Audio;
+using Content.Shared.Charges.Components;
+using Content.Shared.Charges.Systems;
using Content.Shared.Damage;
using Content.Shared.IdentityManagement;
using Content.Shared.Inventory;
@@ -16,6 +18,7 @@ namespace Content.Server.Abilities.Chitinid;
public sealed partial class ChitinidSystem : EntitySystem
{
[Dependency] private readonly SharedActionsSystem _actions = default!;
+ [Dependency] private readonly SharedChargesSystem _charges = default!;
[Dependency] private readonly SharedAudioSystem _audio = default!;
[Dependency] private readonly IPrototypeManager _proto = default!;
[Dependency] private readonly SharedPopupSystem _popup = default!;
@@ -44,15 +47,17 @@ public override void Update(float frameTime)
if (chitinid.AmountAbsorbed >= chitinid.MaximumAbsorbed || _mobState.IsDead(uid))
continue;
- if (_damageable.TryChangeDamage(uid, chitinid.Healing, damageable: damageable) is {} delta)
- {
- chitinid.AmountAbsorbed += -delta.GetTotal().Float();
- if (chitinid.ChitziteAction != null && chitinid.AmountAbsorbed >= chitinid.MaximumAbsorbed)
- {
- _actions.SetCharges(chitinid.ChitziteAction, 1); // You get the charge back and that's it. Tough.
- _actions.SetEnabled(chitinid.ChitziteAction, true);
- }
- }
+ if (_damageable.TryChangeDamage(uid, chitinid.Healing, damageable: damageable) is not { } delta)
+ continue;
+ chitinid.AmountAbsorbed += -delta.GetTotal().Float();
+
+ if (chitinid.ChitziteAction == null || chitinid.AmountAbsorbed < chitinid.MaximumAbsorbed)
+ continue;
+
+ // Null Sector: "This may or may not break. I am not quite sure." -LZ22
+ if (TryComp(chitinid.ChitziteAction.Value, out var comp))
+ _charges.SetCharges((chitinid.ChitziteAction.Value, comp), 1); // You get a charge and that's it. Tough.
+ _actions.SetEnabled(chitinid.ChitziteAction, true);
}
var entQuery = EntityQueryEnumerator();
diff --git a/Content.Server/_DV/Felinid/FelinidSystem.cs b/Content.Server/_DV/Felinid/FelinidSystem.cs
new file mode 100644
index 00000000000..47cb910ee47
--- /dev/null
+++ b/Content.Server/_DV/Felinid/FelinidSystem.cs
@@ -0,0 +1,74 @@
+using Content.Server.Body.Components;
+using Content.Server.Medical;
+using Content.Shared._DV.Abilities;
+using Content.Shared._DV.Abilities.Felinid;
+using Content.Shared.Chemistry.EntitySystems;
+using Content.Shared.Item;
+using Content.Shared.StatusEffect;
+using Content.Shared.Throwing;
+using Robust.Shared.Random;
+
+namespace Content.Server._DV.Abilities.Felinid;
+
+///
+/// Handles felinid logic except for fitting in bags.
+///
+///
+/// This could be moved to shared if:
+/// 1. bloodstream was in shared
+/// 2. vomiting was in shared
+/// 3. this didn't use RNG.
+///
+public sealed class FelinidSystem : SharedFelinidSystem
+{
+ [Dependency] private readonly IRobustRandom _random = default!;
+ [Dependency] private readonly SharedSolutionContainerSystem _solution = default!;
+ [Dependency] private readonly VomitSystem _vomit = default!;
+
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ SubscribeLocalEvent(OnItemCoughedUp);
+
+ SubscribeLocalEvent(OnHairballHit);
+ SubscribeLocalEvent(OnHairballPickupAttempt);
+ }
+
+ private void OnItemCoughedUp(Entity ent, ref ItemCoughedUpEvent args)
+ {
+ if (!TryComp(ent, out var blood) || blood.ChemicalSolution is not {} solution)
+ return;
+
+ var item = args.Item;
+ var hairball = Comp(item);
+ var purged = _solution.SplitSolution(solution, ent.Comp.PurgedQuantity);
+ if (_solution.TryGetSolution(item, hairball.SolutionName, out var hairballSolution))
+ {
+ _solution.TryAddSolution(hairballSolution.Value, purged);
+ }
+ }
+
+ private void OnHairballHit(Entity ent, ref ThrowDoHitEvent args)
+ {
+ TryVomit(ent, args.Target);
+ }
+
+ private void OnHairballPickupAttempt(Entity ent, ref GettingPickedUpAttemptEvent args)
+ {
+ if (TryVomit(ent, args.User))
+ args.Cancel();
+ }
+
+ private bool TryVomit(Entity ent, EntityUid uid)
+ {
+ if (HasComp(uid) || !HasComp(uid))
+ return false;
+
+ if (!_random.Prob(ent.Comp.VomitProb))
+ return false;
+
+ _vomit.Vomit(uid);
+ return true;
+ }
+}
diff --git a/Content.Server/_Null/Components/WarperComponent.cs b/Content.Server/_Null/Components/WarperComponent.cs
new file mode 100644
index 00000000000..746e6697c43
--- /dev/null
+++ b/Content.Server/_Null/Components/WarperComponent.cs
@@ -0,0 +1,47 @@
+using Content.Server._Null.Systems;
+using Robust.Shared.Audio;
+
+namespace Content.Server._Null.Components;
+
+///
+/// Allows an interactable entity to be used as a "teleporter" to a different map.
+///
+[RegisterComponent, AutoGenerateComponentState(fieldDeltas: false), Access(typeof(WarperSystem))]
+public sealed partial class WarperComponent : Component
+{
+ /// Warp destination unique identifier.
+ /// This is specifically set on a Ladder / Entrance with intent to
+ /// -read- this ID to travel-to. It is NOT the ID of the -current- Ladder / Entrance.
+ [ViewVariables(VVAccess.ReadWrite), DataField("id")]
+ public string? CurrentId { get; set; } = string.Empty;
+
+ /// Warp destination unique identifier.
+ /// This is specifically set on a Ladder / Entrance with intent to
+ /// -read- this ID to travel-to. It is NOT the ID of the -current- Ladder / Entrance.
+ [ViewVariables(VVAccess.ReadWrite), DataField("destination")]
+ public string? DestinationId { get; set; } = string.Empty;
+
+ [ViewVariables(VVAccess.ReadWrite), DataField("hostileFactions")]
+ public List HostileFactions { get; set; } =
+ [
+ // SS14 - 20251120
+ "Dragon", "Xeno", "Zombie", "SimpleHostile", "AllHostile",
+ ];
+
+ /// Does the level need to be completed before it can be used?
+ [DataField("levelClearRequired"), AutoNetworkedField, ViewVariables(VVAccess.ReadWrite)]
+ public bool LevelClearRequired { get; set; } = false;
+
+ ///
+ /// Assists with determining reverse-generation order for dungeons.
+ /// IE: Spawning on a lower layer and working one's way up.
+ ///
+ [DataField("sealed"), ViewVariables(VVAccess.ReadWrite)]
+ public bool Sealed { get; set; } = false;
+
+ ///
+ /// The sound played after players are shuffled/teleported around
+ ///
+ [DataField("teleportSound"), ViewVariables(VVAccess.ReadWrite)]
+ public SoundSpecifier TeleportSound = new SoundPathSpecifier("/Audio/Effects/teleport_arrival.ogg");
+}
diff --git a/Content.Server/_Null/Systems/WarperSystem.cs b/Content.Server/_Null/Systems/WarperSystem.cs
new file mode 100644
index 00000000000..9241013b818
--- /dev/null
+++ b/Content.Server/_Null/Systems/WarperSystem.cs
@@ -0,0 +1,261 @@
+using System.Linq;
+using System.Numerics;
+using Content.Server._Null.Components;
+using Content.Server.Administration;
+using Content.Server.Bible.Components;
+using Content.Server.Popups;
+using Content.Shared.Administration;
+using Content.Shared.Examine;
+using Content.Shared.Ghost;
+using Content.Shared.Interaction;
+using Content.Shared.Mobs.Systems;
+using Content.Shared.NPC.Components;
+using Content.Shared.Popups;
+using Content.Shared.Tag;
+using Robust.Server.Audio;
+using Robust.Shared.Console;
+using Robust.Shared.Map;
+using Robust.Shared.Physics.Components;
+using Robust.Shared.Physics.Systems;
+
+#pragma warning disable CS0618 // Type or member is obsolete
+
+namespace Content.Server._Null.Systems;
+
+///
+/// This is lifted from Mining Station 14 and redesigned to be used for Vault Station's Dungeon Layers.
+/// However, it was repurposed for the Null Sector by LukeZurg22, who wrote most of this code anyhow.
+///
+/// As such, this system is designed to accomodate this arrangement and manual changes will be
+/// necessary in order to accomplish procedural-only levels.
+/// -Z
+///
+public sealed class WarperSystem : EntitySystem
+{
+ [Dependency] private readonly PopupSystem _popupSystem = default!;
+ [Dependency] private readonly SharedPhysicsSystem _physics = default!;
+ [Dependency] private readonly MobStateSystem _mobState = default!;
+ [Dependency] private readonly TagSystem _tags = default!;
+ [Dependency] private readonly AudioSystem _audio = default!;
+
+ public override void Initialize()
+ {
+ base.Initialize();
+ SubscribeLocalEvent(OnActivate);
+ SubscribeLocalEvent(OnExamined);
+ }
+
+ private void OnExamined(Entity ent, ref ExaminedEvent args)
+ {
+ // Show source ID destination ID message.
+ args.PushMarkup(string.Concat(!string.IsNullOrEmpty(ent.Comp.CurrentId)
+ ? Loc.GetString($"warper-on-examine-source", ("location", ent.Comp.CurrentId))
+ : Loc.GetString($"warper-cancelled-no-source"),
+ !string.IsNullOrEmpty(ent.Comp.DestinationId)
+ ? Loc.GetString($"warper-on-examine-destination", ("location", ent.Comp.DestinationId))
+ : Loc.GetString($"warper-cancelled-no-destination")));
+ }
+
+ private void OnActivate(Entity ent, ref ActivateInWorldEvent args)
+ {
+ TryWarp(ent, args.Target, args.User);
+ }
+
+
+ ///
+ /// Gets a warper using an id, which may be a source or destination ID. This assumes
+ /// it is a destination ID by default.
+ ///
+ /// The first warper with a provided destination.
+ private Entity? GetWarper(string id, bool useSourceId = false)
+ {
+ foreach (var warper in EntityManager.EntityQuery())
+ {
+ if (useSourceId && warper.CurrentId == null)
+ continue;
+ if (warper.DestinationId == null)
+ continue;
+
+ if (useSourceId)
+ {
+ if (warper.DestinationId!.Equals(id))
+ return new Entity(warper.Owner, warper);
+ }
+ else
+ {
+ if (warper.CurrentId!.Equals(id))
+ return new Entity(warper.Owner, warper);
+ }
+ }
+
+ return null;
+ }
+
+ private void DisplayLocale(EntityUid? user, string locale)
+ {
+ if (user == null)
+ return;
+ var message = Loc.GetString(locale);
+ _popupSystem.PopupEntity(message, user.Value, PopupType.Medium);
+ }
+
+ ///
+ /// Checks whether a dungeon level is complete if a certain amount of monsters within the dungeon are dead.
+ /// This is specific to all monsters containing one or more faction tags.
+ ///
+ ///
+ ///
+ ///
+ private bool CanWarp(Entity warper, EntityUid? user = null)
+ {
+ // If it's sealed, it simply cannot warp.
+ if (warper.Comp.Sealed)
+ {
+ DisplayLocale(user, "warper-cancelled-sealed");
+ return false;
+ }
+
+ // If there is no assigned destination, then the user can't warp.
+ if (warper.Comp.DestinationId == null)
+ {
+ DisplayLocale(user, "warper-cancelled-no-destination");
+ return false;
+ }
+
+ var dest = GetWarper(warper.Comp.DestinationId);
+ if (dest is null)
+ {
+ DisplayLocale(user, "warper-cancelled-invalid-destination");
+ return false;
+ }
+
+ // If it isn't sealed, and there isn't any need to clean hostiles, then warp anyway.
+ if (!warper.Comp.LevelClearRequired)
+ {
+ return true;
+ }
+
+ var hostileFactions = warper.Comp.HostileFactions;
+ int monsterCount = 0, aliveCount = 0;
+ foreach (var mob in EntityManager.EntityQuery())
+ {
+ // NPCs not on the same map - skipped // TODO: Rewrite this to accomodate grid, instead.
+ if (Transform(mob.Owner).GridUid == Transform(warper).GridUid)
+ continue;
+ // NPC not of a hostile faction - skipped
+ if (!mob.Factions.Any(faction => hostileFactions.Contains(faction.ToString())))
+ continue;
+ // NPC is a pet - skipped
+ if (HasComp(mob.Owner))
+ continue;
+ // Add to monster count.
+ monsterCount++;
+ // If it's dead, don't continue.
+ if (_mobState.IsDead(mob.Owner))
+ continue;
+ // Monster is a Boss - dungeon is straight up NOT DONE.
+ if (_tags.HasTag(mob.Owner, "Boss"))
+ return false;
+ // So if it's not a pet, not dead, not a boss,
+ aliveCount++;
+ }
+
+ // 20% bottom limit for how many hostiles could be alive to be considered complete.
+ if (aliveCount <= 0.2 * monsterCount)
+ return true;
+
+ return false;
+ }
+
+ private void TryWarp(Entity warper, EntityUid user, EntityUid victim)
+ {
+ if (!CanWarp(warper, user))
+ return;
+
+ if (string.IsNullOrEmpty(warper.Comp.DestinationId))
+ {
+ DisplayLocale(user, "warper-cancelled-no-destination");
+ return;
+ }
+
+ var destination = GetWarper(warper.Comp.DestinationId);
+ var entityManager = IoCManager.Resolve();
+ entityManager.TryGetComponent(destination, out TransformComponent? destXform);
+ if (destXform is null)
+ {
+ Logger.DebugS("warper", $"Warp destination '{warper.Comp.DestinationId}' has no transform");
+ var message = Loc.GetString("warper-map-invalid");
+ _popupSystem.PopupEntity(message, user);
+ return;
+ }
+
+ // Check that the destination map is initialized and return unless in aghost mode.
+ var mapManager = IoCManager.Resolve();
+ var destinationMap = destXform.MapID;
+ if (!mapManager.IsMapInitialized(destinationMap) || mapManager.IsMapPaused(destinationMap))
+ {
+ if (!entityManager.HasComponent(user))
+ {
+ // Normal ghosts cannot interact, so if we're here this is already an admin ghost.
+ Logger.DebugS("warper",
+ $"Player tried to warp to '{warper.Comp.DestinationId}', which is not on a running map");
+ DisplayLocale(user, "warper-map-invalid");
+ return;
+ }
+ }
+
+ _audio.PlayPvs(warper.Comp.TeleportSound, user);
+ var transform = entityManager.GetComponent(victim);
+ transform.Coordinates = destXform.Coordinates;
+ transform.AttachToGridOrMap();
+ if (entityManager.TryGetComponent(victim, out PhysicsComponent? _))
+ {
+ _physics.SetLinearVelocity(victim, Vector2.Zero);
+ }
+ }
+
+ [AdminCommand(AdminFlags.Debug)]
+ private sealed class WarpToCommand : IConsoleCommand
+ {
+ public string Command => "warpto";
+ public string Description => "Finds the nearest warper and attempts to warp to it.";
+ public string Help => "warpto";
+
+ public void Execute(IConsoleShell shell, string argStr, string[] args)
+ {
+ if (shell.Player?.AttachedEntity == null /*|| shell.Player.AttachedEntityTransform == null*/)
+ {
+ shell.WriteLine("You need a player and attached entity to use this command.");
+ return;
+ }
+
+ if (args.Length < 1)
+ {
+ shell.WriteLine("Invalid argument length. This requires a destination ID.");
+ return;
+ }
+
+ var destination = args[0];
+
+ var sysMan = IoCManager.Resolve();
+ var warpSystem = sysMan.GetEntitySystem();
+ var player = shell.Player.AttachedEntity.Value;
+ var warper = warpSystem.GetWarper(destination);
+
+ if (warper == null)
+ {
+ shell.WriteLine("No warp found!");
+ return;
+ }
+
+ if (!warpSystem.CanWarp(warper.Value, player))
+ {
+ shell.WriteLine("Cannot warp!");
+ return;
+ }
+
+ var evt = new ActivateInWorldEvent(player, warper.Value.Comp.Owner, false);
+ warpSystem.TryWarp(warper.Value, evt.Target, evt.User);
+ }
+ }
+}
diff --git a/Content.Shared/Actions/SharedActionsSystem.cs b/Content.Shared/Actions/SharedActionsSystem.cs
index a476ee5b0b0..96f1fd0e44b 100644
--- a/Content.Shared/Actions/SharedActionsSystem.cs
+++ b/Content.Shared/Actions/SharedActionsSystem.cs
@@ -23,14 +23,14 @@ public abstract class SharedActionsSystem : EntitySystem
{
[Dependency] protected readonly IGameTiming GameTiming = default!;
[Dependency] private readonly INetManager _net = default!; // Goobstation
- [Dependency] private readonly ISharedAdminLogManager _adminLogger = default!;
- [Dependency] private readonly SharedInteractionSystem _interactionSystem = default!;
- [Dependency] private readonly ActionBlockerSystem _actionBlockerSystem = default!;
- [Dependency] private readonly RotateToFaceSystem _rotateToFaceSystem = default!;
- [Dependency] private readonly SharedAudioSystem _audio = default!;
- [Dependency] private readonly SharedTransformSystem _transformSystem = default!;
- [Dependency] private readonly ActionContainerSystem _actionContainer = default!;
- [Dependency] private readonly EntityWhitelistSystem _whitelistSystem = default!;
+ [Dependency] private readonly ISharedAdminLogManager _adminLogger = default!;
+ [Dependency] private readonly ActionBlockerSystem _actionBlockerSystem = default!;
+ [Dependency] private readonly ActionContainerSystem _actionContainer = default!;
+ [Dependency] private readonly EntityWhitelistSystem _whitelistSystem = default!;
+ [Dependency] private readonly RotateToFaceSystem _rotateToFaceSystem = default!;
+ [Dependency] private readonly SharedAudioSystem _audio = default!;
+ [Dependency] private readonly SharedInteractionSystem _interactionSystem = default!;
+ [Dependency] private readonly SharedTransformSystem _transformSystem = default!;
public override void Initialize()
{
@@ -71,47 +71,9 @@ public override void Initialize()
SubscribeAllEvent(OnActionRequest);
}
- public override void Update(float frameTime)
- {
- base.Update(frameTime);
-
- var worldActionQuery = EntityQueryEnumerator();
- while (worldActionQuery.MoveNext(out var uid, out var action))
- {
- if (IsCooldownActive(action) || !ShouldResetCharges(action))
- continue;
-
- ResetCharges(uid, dirty: true);
- }
-
- var instantActionQuery = EntityQueryEnumerator();
- while (instantActionQuery.MoveNext(out var uid, out var action))
- {
- if (IsCooldownActive(action) || !ShouldResetCharges(action))
- continue;
-
- ResetCharges(uid, dirty: true);
- }
-
- var entityActionQuery = EntityQueryEnumerator();
- while (entityActionQuery.MoveNext(out var uid, out var action))
- {
- if (IsCooldownActive(action) || !ShouldResetCharges(action))
- continue;
-
- ResetCharges(uid, dirty: true);
- }
- }
-
private void OnActionMapInit(EntityUid uid, BaseActionComponent component, MapInitEvent args)
{
component.OriginalIconColor = component.IconColor;
-
- if (component.Charges == null)
- return;
-
- component.MaxCharges ??= component.Charges.Value;
- Dirty(uid, component);
}
private void OnActionShutdown(EntityUid uid, BaseActionComponent component, ComponentShutdown args)
@@ -314,68 +276,6 @@ public void SetEnabled(EntityUid? actionId, bool enabled)
Dirty(actionId.Value, action);
}
- public void SetCharges(EntityUid? actionId, int? charges)
- {
- if (!TryGetActionData(actionId, out var action) ||
- action.Charges == charges)
- {
- return;
- }
-
- action.Charges = charges;
- UpdateAction(actionId, action);
- Dirty(actionId.Value, action);
- }
-
- public int? GetCharges(EntityUid? actionId)
- {
- if (!TryGetActionData(actionId, out var action))
- return null;
-
- return action.Charges;
- }
-
- public void AddCharges(EntityUid? actionId, int addCharges)
- {
- if (!TryGetActionData(actionId, out var action) || action.Charges == null || addCharges < 1)
- return;
-
- action.Charges += addCharges;
- UpdateAction(actionId, action);
- Dirty(actionId.Value, action);
- }
-
- public void RemoveCharges(EntityUid? actionId, int? removeCharges)
- {
- if (!TryGetActionData(actionId, out var action) || action.Charges == null)
- return;
-
- if (removeCharges == null)
- action.Charges = removeCharges;
- else
- action.Charges -= removeCharges;
-
- if (action.Charges is < 0)
- action.Charges = null;
-
- UpdateAction(actionId, action);
- Dirty(actionId.Value, action);
- }
-
- public void ResetCharges(EntityUid? actionId, bool update = false, bool dirty = false)
- {
- if (!TryGetActionData(actionId, out var action))
- return;
-
- action.Charges = action.MaxCharges;
-
- if (update)
- UpdateAction(actionId, action);
-
- if (dirty)
- Dirty(actionId.Value, action);
- }
-
private void OnActionsGetState(EntityUid uid, ActionsComponent component, ref ComponentGetState args)
{
args.State = new ActionsComponentState(GetNetEntitySet(component.Actions));
@@ -418,6 +318,10 @@ private void OnActionRequest(RequestPerformActionEvent ev, EntitySessionEventArg
if (!action.Enabled)
return;
+ var curTime = GameTiming.CurTime;
+ if (IsCooldownActive(action, curTime))
+ return;
+
// check for action use prevention
// TODO: make code below use this event with a dedicated component
var attemptEv = new ActionAttemptEvent(user);
@@ -425,14 +329,6 @@ private void OnActionRequest(RequestPerformActionEvent ev, EntitySessionEventArg
if (attemptEv.Cancelled)
return;
- var curTime = GameTiming.CurTime;
- if (IsCooldownActive(action, curTime))
- return;
-
- // TODO: Replace with individual charge recovery when we have the visuals to aid it
- if (action is { Charges: < 1, RenewCharges: true })
- ResetCharges(actionEnt, true, true);
-
BaseActionEvent? performEvent = null;
if (action.CheckConsciousness && !_actionBlockerSystem.CanConsciouslyPerformAction(user))
@@ -707,16 +603,8 @@ public void PerformAction(EntityUid performer, ActionsComponent? component, Enti
var dirty = toggledBefore != action.Toggled;
- if (action.Charges != null)
- {
- dirty = true;
- action.Charges--;
- if (action is { Charges: 0, RenewCharges: false })
- action.Enabled = false;
- }
-
action.Cooldown = null;
- if (action is { UseDelay: not null, Charges: null or < 1 })
+ if (action is { UseDelay: not null})
{
dirty = true;
action.Cooldown = (curTime, curTime + action.UseDelay.Value);
@@ -1016,8 +904,6 @@ public bool ValidAction(BaseActionComponent action, bool canReach = true)
if (!action.Enabled)
return false;
- if (action.Charges.HasValue && action.Charges <= 0)
- return false;
var curTime = GameTiming.CurTime;
if (action.Cooldown.HasValue && action.Cooldown.Value.End > curTime)
@@ -1127,15 +1013,9 @@ public void SetEntityIcon(EntityUid uid, EntityUid? icon, BaseActionComponent? a
///
/// Checks if the action has a cooldown and if it's still active
///
- protected bool IsCooldownActive(BaseActionComponent action, TimeSpan? curTime = null)
+ public bool IsCooldownActive(BaseActionComponent action, TimeSpan? curTime = null)
{
- curTime ??= GameTiming.CurTime;
// TODO: Check for charge recovery timer
return action.Cooldown.HasValue && action.Cooldown.Value.End > curTime;
}
-
- protected bool ShouldResetCharges(BaseActionComponent action)
- {
- return action is { Charges: < 1, RenewCharges: true };
- }
}
diff --git a/Content.Shared/Atmos/EntitySystems/SharedAtmosPipeLayersSystem.cs b/Content.Shared/Atmos/EntitySystems/SharedAtmosPipeLayersSystem.cs
index 7d7e39cef6e..5d83857455b 100644
--- a/Content.Shared/Atmos/EntitySystems/SharedAtmosPipeLayersSystem.cs
+++ b/Content.Shared/Atmos/EntitySystems/SharedAtmosPipeLayersSystem.cs
@@ -124,14 +124,16 @@ private void OnInteractUsing(Entity ent, ref InteractU
if (ent.Comp.NumberOfPipeLayers <= 1 || ent.Comp.PipeLayersLocked)
return;
+ if (!TryComp(args.Used, out var tool) || !_tool.HasQuality(args.Used, ent.Comp.Tool, tool))
+ return;
+
if (TryComp(ent, out var subFloorHide) && subFloorHide.IsUnderCover)
{
- _popup.PopupPredicted(Loc.GetString("atmos-pipe-layers-component-cannot-adjust-pipes"), ent, args.User);
+ _popup.PopupClient(Loc.GetString("atmos-pipe-layers-component-cannot-adjust-pipes"), ent, args.User);
return;
}
- if (TryComp(args.Used, out var tool) && _tool.HasQuality(args.Used, ent.Comp.Tool, tool))
- _tool.UseTool(args.Used, args.User, ent, ent.Comp.Delay, tool.Qualities, new TrySetNextPipeLayerCompletedEvent());
+ _tool.UseTool(args.Used, args.User, ent, ent.Comp.Delay, tool.Qualities, new TrySetNextPipeLayerCompletedEvent());
}
private void OnUseInHandEvent(Entity ent, ref UseInHandEvent args)
@@ -146,7 +148,7 @@ private void OnUseInHandEvent(Entity ent, ref UseInHan
var toolName = Loc.GetString(toolProto.ToolName).ToLower();
var message = Loc.GetString("atmos-pipe-layers-component-tool-missing", ("toolName", toolName));
- _popup.PopupPredicted(message, ent, args.User);
+ _popup.PopupClient(message, ent, args.User);
}
return;
@@ -222,7 +224,7 @@ public virtual void SetPipeLayer(Entity ent, AtmosPipe
var layerName = GetPipeLayerName(ent.Comp.CurrentPipeLayer);
var message = Loc.GetString("atmos-pipe-layers-component-change-layer", ("layerName", layerName));
- _popup.PopupPredicted(message, ent, user);
+ _popup.PopupClient(message, ent, user);
}
}
diff --git a/Content.Shared/Atmos/IGasMixtureHolder.cs b/Content.Shared/Atmos/IGasMixtureHolder.cs
new file mode 100644
index 00000000000..5ad100319db
--- /dev/null
+++ b/Content.Shared/Atmos/IGasMixtureHolder.cs
@@ -0,0 +1,6 @@
+namespace Content.Shared.Atmos;
+
+public interface IGasMixtureHolder
+{
+ public GasMixture Air { get; set; }
+}
\ No newline at end of file
diff --git a/Content.Server/Atmos/Piping/Unary/Components/GasCanisterComponent.cs b/Content.Shared/Atmos/Piping/Unary/Components/GasCanisterComponent.cs
similarity index 96%
rename from Content.Server/Atmos/Piping/Unary/Components/GasCanisterComponent.cs
rename to Content.Shared/Atmos/Piping/Unary/Components/GasCanisterComponent.cs
index afbfb912497..f0713b39fba 100644
--- a/Content.Server/Atmos/Piping/Unary/Components/GasCanisterComponent.cs
+++ b/Content.Shared/Atmos/Piping/Unary/Components/GasCanisterComponent.cs
@@ -1,9 +1,8 @@
-using Content.Shared.Atmos;
using Content.Shared.Containers.ItemSlots;
using Content.Shared.Guidebook;
using Robust.Shared.Audio;
-namespace Content.Server.Atmos.Piping.Unary.Components
+namespace Content.Shared.Atmos.Piping.Unary.Components
{
[RegisterComponent]
public sealed partial class GasCanisterComponent : Component, IGasMixtureHolder
diff --git a/Content.Shared/Charges/Components/AutoRechargeComponent.cs b/Content.Shared/Charges/Components/AutoRechargeComponent.cs
new file mode 100644
index 00000000000..704783056c5
--- /dev/null
+++ b/Content.Shared/Charges/Components/AutoRechargeComponent.cs
@@ -0,0 +1,19 @@
+using Content.Shared.Charges.Systems;
+using Robust.Shared.GameStates;
+
+namespace Content.Shared.Charges.Components;
+
+///
+/// Something with limited charges that can be recharged automatically.
+/// Requires LimitedChargesComponent to function.
+///
+[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
+[Access(typeof(SharedChargesSystem))]
+public sealed partial class AutoRechargeComponent : Component
+{
+ ///
+ /// The time it takes to regain a single charge
+ ///
+ [DataField, AutoNetworkedField]
+ public TimeSpan RechargeDuration = TimeSpan.FromSeconds(90);
+}
diff --git a/Content.Shared/Charges/Components/LimitedChargesComponent.cs b/Content.Shared/Charges/Components/LimitedChargesComponent.cs
index 6973ffbe72e..ff926fc158e 100644
--- a/Content.Shared/Charges/Components/LimitedChargesComponent.cs
+++ b/Content.Shared/Charges/Components/LimitedChargesComponent.cs
@@ -1,24 +1,27 @@
using Content.Shared.Charges.Systems;
using Robust.Shared.GameStates;
+using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
namespace Content.Shared.Charges.Components;
-[RegisterComponent, NetworkedComponent]
-[Access(typeof(SharedChargesSystem))]
-[AutoGenerateComponentState]
+///
+/// Specifies the attached action has discrete charges, separate to a cooldown.
+///
+[RegisterComponent, NetworkedComponent, AutoGenerateComponentState, Access(typeof(SharedChargesSystem))]
public sealed partial class LimitedChargesComponent : Component
{
+ [DataField, AutoNetworkedField]
+ public int LastCharges;
+
///
- /// The maximum number of charges
+ /// The max charges this action has.
///
- [DataField("maxCharges"), ViewVariables(VVAccess.ReadWrite)]
- [AutoNetworkedField]
- public int MaxCharges = 3;
+ [DataField, AutoNetworkedField, Access(Other = AccessPermissions.Read)]
+ public int MaxCharges = 1;
///
- /// The current number of charges
+ /// Last time charges was changed. Used to derive current charges.
///
- [DataField("charges"), ViewVariables(VVAccess.ReadWrite)]
- [AutoNetworkedField]
- public int Charges = 3;
+ [DataField(customTypeSerializer:typeof(TimeOffsetSerializer)), AutoNetworkedField]
+ public TimeSpan LastUpdate;
}
diff --git a/Content.Shared/Charges/Systems/SharedChargesSystem.cs b/Content.Shared/Charges/Systems/SharedChargesSystem.cs
index 7f95ef184e4..9fde7cd10d9 100644
--- a/Content.Shared/Charges/Systems/SharedChargesSystem.cs
+++ b/Content.Shared/Charges/Systems/SharedChargesSystem.cs
@@ -1,103 +1,233 @@
+using Content.Shared.Actions.Events;
using Content.Shared.Charges.Components;
using Content.Shared.Examine;
+using JetBrains.Annotations;
+using Robust.Shared.Timing;
namespace Content.Shared.Charges.Systems;
public abstract class SharedChargesSystem : EntitySystem
{
- protected EntityQuery Query;
+ [Dependency] protected readonly IGameTiming _timing = default!;
+
+ /*
+ * Despite what a bunch of systems do you don't need to continuously tick linear number updates and can just derive it easily.
+ */
public override void Initialize()
{
base.Initialize();
- Query = GetEntityQuery();
-
SubscribeLocalEvent(OnExamine);
+
+ SubscribeLocalEvent(OnChargesAttempt);
+ SubscribeLocalEvent(OnChargesMapInit);
+ SubscribeLocalEvent(OnChargesPerformed);
}
- protected virtual void OnExamine(EntityUid uid, LimitedChargesComponent comp, ExaminedEvent args)
+ private void OnExamine(EntityUid uid, LimitedChargesComponent comp, ExaminedEvent args)
{
if (!args.IsInDetailsRange)
return;
- using (args.PushGroup(nameof(LimitedChargesComponent)))
+ var rechargeEnt = new Entity(uid, comp, null);
+ var charges = GetCurrentCharges(rechargeEnt);
+ using var _ = args.PushGroup(nameof(LimitedChargesComponent));
+
+ args.PushMarkup(Loc.GetString("limited-charges-charges-remaining", ("charges", charges)));
+ if (charges == comp.MaxCharges)
{
- args.PushMarkup(Loc.GetString("limited-charges-charges-remaining", ("charges", comp.Charges)));
- if (comp.Charges == comp.MaxCharges)
- {
- args.PushMarkup(Loc.GetString("limited-charges-max-charges"));
- }
+ args.PushMarkup(Loc.GetString("limited-charges-max-charges"));
}
+
+ // only show the recharging info if it's not full
+ if (charges == comp.MaxCharges || !TryComp(uid, out var recharge))
+ return;
+
+ rechargeEnt.Comp2 = recharge;
+ var timeRemaining = GetNextRechargeTime(rechargeEnt);
+ args.PushMarkup(Loc.GetString("limited-charges-recharging", ("seconds", timeRemaining.TotalSeconds.ToString("F1"))));
}
- ///
- /// Tries to add a number of charges. If it over or underflows it will be clamped, wasting the extra charges.
- ///
- public virtual void AddCharges(EntityUid uid, int change, LimitedChargesComponent? comp = null)
+ private void OnChargesAttempt(Entity ent, ref ActionAttemptEvent args)
{
- if (!Query.Resolve(uid, ref comp, false))
+ if (args.Cancelled)
return;
- var old = comp.Charges;
- comp.Charges = Math.Clamp(comp.Charges + change, 0, comp.MaxCharges);
- if (comp.Charges != old)
- Dirty(uid, comp);
+ var charges = GetCurrentCharges((ent.Owner, ent.Comp, null));
+
+ if (charges <= 0)
+ {
+ args.Cancelled = true;
+ }
+ }
+
+ private void OnChargesPerformed(Entity ent, ref ActionPerformedEvent args)
+ {
+ AddCharges((ent.Owner, ent.Comp), -1);
+ }
+
+ private void OnChargesMapInit(Entity ent, ref MapInitEvent args)
+ {
+ // If nothing specified use max.
+ if (ent.Comp.LastCharges == 0)
+ {
+ ent.Comp.LastCharges = ent.Comp.MaxCharges;
+ }
+ // If -1 used then we don't want any.
+ else if (ent.Comp.LastCharges < 0)
+ {
+ ent.Comp.LastCharges = 0;
+ }
+
+ ent.Comp.LastUpdate = _timing.CurTime;
+ Dirty(ent);
+ }
+
+ [Pure]
+ public bool HasCharges(Entity action, int charges)
+ {
+ var current = GetCurrentCharges(action);
+
+ return current >= charges;
}
///
- /// Gets the limited charges component and returns true if there are no charges. Will return false if there is no limited charges component.
+ /// Adds the specified charges. Does not reset the accumulator.
///
- public bool IsEmpty(EntityUid uid, LimitedChargesComponent? comp = null)
+ public void AddCharges(Entity action, int addCharges)
+ {
+ if (addCharges == 0)
+ return;
+
+ action.Comp ??= EnsureComp(action.Owner);
+
+ // 1. If we're going FROM max then set lastupdate to now (so it doesn't instantly recharge).
+ // 2. If we're going TO max then also set lastupdate to now.
+ // 3. Otherwise don't modify it.
+ // No idea if we go to 0 but future problem.
+
+ var lastCharges = GetCurrentCharges(action);
+ var charges = lastCharges + addCharges;
+
+ if (lastCharges == charges)
+ return;
+
+ if (charges == action.Comp.MaxCharges || lastCharges == action.Comp.MaxCharges)
+ {
+ action.Comp.LastUpdate = _timing.CurTime;
+ }
+
+ action.Comp.LastCharges = Math.Clamp(action.Comp.LastCharges + addCharges, 0, action.Comp.MaxCharges);
+ Dirty(action);
+ }
+
+ public bool TryUseCharge(Entity entity)
{
- // can't be empty if there are no limited charges
- if (!Query.Resolve(uid, ref comp, false))
+ return TryUseCharges(entity, 1);
+ }
+
+ public bool TryUseCharges(Entity entity, int amount)
+ {
+ var current = GetCurrentCharges(entity);
+
+ if (current < amount)
+ {
return false;
+ }
- return comp.Charges <= 0;
+ AddCharges(entity, -amount);
+ return true;
}
- ///
- /// Uses a single charge. Must check IsEmpty beforehand to prevent using with 0 charge.
- ///
- public void UseCharge(EntityUid uid, LimitedChargesComponent? comp = null)
+ [Pure]
+ public bool IsEmpty(Entity entity)
{
- AddCharges(uid, -1, comp);
+ return GetCurrentCharges(entity) == 0;
}
///
- /// Checks IsEmpty and uses a charge if it isn't empty.
+ /// Resets action charges to MaxCharges.
///
- public bool TryUseCharge(Entity ent)
+ public void ResetCharges(Entity action)
{
- if (!Query.Resolve(ent, ref ent.Comp, false))
- return true;
+ if (!Resolve(action.Owner, ref action.Comp, false))
+ return;
- if (IsEmpty(ent, ent.Comp))
- return false;
+ var charges = GetCurrentCharges((action.Owner, action.Comp, null));
- UseCharge(ent, ent.Comp);
- return true;
+ if (charges == action.Comp.MaxCharges)
+ return;
+
+ action.Comp.LastCharges = action.Comp.MaxCharges;
+ action.Comp.LastUpdate = _timing.CurTime;
+ Dirty(action);
+ }
+
+
+ public void SetCharges(Entity action, int value)
+ {
+ action.Comp ??= EnsureComp(action.Owner);
+
+ var adjusted = Math.Clamp(value, 0, action.Comp.MaxCharges);
+
+ if (action.Comp.LastCharges == adjusted)
+ {
+ return;
+ }
+
+ action.Comp.LastCharges = adjusted;
+ action.Comp.LastUpdate = _timing.CurTime;
+ Dirty(action);
}
///
- /// Gets the limited charges component and returns true if the number of charges remaining is less than the specified value.
- /// Will return false if there is no limited charges component.
+ /// The next time a charge will be considered to be filled.
///
- public bool HasInsufficientCharges(EntityUid uid, int requiredCharges, LimitedChargesComponent? comp = null)
+ /// 0 timespan if invalid or no charges to generate.
+ [Pure]
+ public TimeSpan GetNextRechargeTime(Entity entity)
{
- // can't be empty if there are no limited charges
- if (!Resolve(uid, ref comp, false))
- return false;
+ if (!Resolve(entity.Owner, ref entity.Comp1, ref entity.Comp2, false))
+ {
+ return TimeSpan.Zero;
+ }
- return comp.Charges < requiredCharges;
+ // Okay so essentially we need to get recharge time to full, then modulus that by the recharge timer which should be the next tick.
+ var fullTime = ((entity.Comp1.MaxCharges - entity.Comp1.LastCharges) * entity.Comp2.RechargeDuration) + entity.Comp1.LastUpdate;
+ var timeRemaining = fullTime - _timing.CurTime;
+
+ if (timeRemaining < TimeSpan.Zero)
+ {
+ return TimeSpan.Zero;
+ }
+
+ var nextChargeTime = timeRemaining.TotalSeconds % entity.Comp2.RechargeDuration.TotalSeconds;
+ return TimeSpan.FromSeconds(nextChargeTime);
}
///
- /// Uses up a specified number of charges. Must check HasInsufficentCharges beforehand to prevent using with insufficient remaining charges.
+ /// Derives the current charges of an entity.
///
- public virtual void UseCharges(EntityUid uid, int chargesUsed, LimitedChargesComponent? comp = null)
+ [Pure]
+ public int GetCurrentCharges(Entity entity)
{
- AddCharges(uid, -chargesUsed, comp);
+ if (!Resolve(entity.Owner, ref entity.Comp1, false))
+ {
+ // I'm all in favor of nullable ints however null-checking return args against comp nullability is dodgy
+ // so we get this.
+ return -1;
+ }
+
+ var calculated = 0;
+
+ if (Resolve(entity.Owner, ref entity.Comp2, false) && entity.Comp2.RechargeDuration.TotalSeconds != 0.0)
+ {
+ calculated = (int)((_timing.CurTime - entity.Comp1.LastUpdate).TotalSeconds / entity.Comp2.RechargeDuration.TotalSeconds);
+ }
+
+ return Math.Clamp(entity.Comp1.LastCharges + calculated,
+ 0,
+ entity.Comp1.MaxCharges);
}
}
diff --git a/Content.Shared/Content.Shared.csproj b/Content.Shared/Content.Shared.csproj
index cac7ed0bf15..1dd0f08a839 100644
--- a/Content.Shared/Content.Shared.csproj
+++ b/Content.Shared/Content.Shared.csproj
@@ -16,6 +16,7 @@
false
+
false
diff --git a/Content.Shared/Doors/Components/DoorComponent.cs b/Content.Shared/Doors/Components/DoorComponent.cs
index a8cb25782ed..64b4ab1857e 100644
--- a/Content.Shared/Doors/Components/DoorComponent.cs
+++ b/Content.Shared/Doors/Components/DoorComponent.cs
@@ -317,7 +317,6 @@ public enum DoorVisuals : byte
BoltLights,
EmergencyLights,
ClosedLights,
- BaseRSI,
}
public enum DoorVisualLayers : byte
diff --git a/Content.Shared/Emag/Systems/EmagSystem.cs b/Content.Shared/Emag/Systems/EmagSystem.cs
index 2967f1d1c66..951cbd920a9 100644
--- a/Content.Shared/Emag/Systems/EmagSystem.cs
+++ b/Content.Shared/Emag/Systems/EmagSystem.cs
@@ -21,7 +21,7 @@ namespace Content.Shared.Emag.Systems;
public sealed class EmagSystem : EntitySystem
{
[Dependency] private readonly ISharedAdminLogManager _adminLogger = default!;
- [Dependency] private readonly SharedChargesSystem _charges = default!;
+ [Dependency] private readonly SharedChargesSystem _sharedCharges = default!;
[Dependency] private readonly SharedPopupSystem _popup = default!;
[Dependency] private readonly TagSystem _tag = default!;
[Dependency] private readonly SharedAudioSystem _audio = default!;
@@ -66,8 +66,8 @@ public bool TryEmagEffect(Entity ent, EntityUid user, EntityUid
if (_tag.HasTag(target, ent.Comp.EmagImmuneTag))
return false;
- TryComp(ent, out var charges);
- if (_charges.IsEmpty(ent, charges))
+ Entity chargesEnt = ent.Owner;
+ if (_sharedCharges.IsEmpty(chargesEnt))
{
_popup.PopupClient(Loc.GetString("emag-no-charges"), user, user);
return false;
@@ -85,8 +85,8 @@ public bool TryEmagEffect(Entity ent, EntityUid user, EntityUid
_adminLogger.Add(LogType.Emag, LogImpact.High, $"{ToPrettyString(user):player} emagged {ToPrettyString(target):target} with flag(s): {ent.Comp.EmagType}");
- if (charges != null && emaggedEvent.Handled)
- _charges.UseCharge(ent, charges);
+ if (emaggedEvent.Handled)
+ _sharedCharges.TryUseCharge(chargesEnt);
if (!emaggedEvent.Repeatable)
{
@@ -112,7 +112,7 @@ public bool TryUnemagEffect(Entity ent, EntityUid user, EntityUi
return false;
TryComp(ent, out var charges);
- if (_charges.IsEmpty(ent, charges))
+ if (_sharedCharges.IsEmpty((ent, charges)))
{
_popup.PopupClient(Loc.GetString("emag-no-charges"), user, user);
return false;
@@ -131,7 +131,7 @@ public bool TryUnemagEffect(Entity ent, EntityUid user, EntityUi
_adminLogger.Add(LogType.Emag, LogImpact.Medium, $"{ToPrettyString(user):player} demagged {ToPrettyString(target):target} with flag(s): {ent.Comp.EmagType}");
if (charges != null && emaggedEvent.Handled)
- _charges.UseCharge(ent, charges);
+ _sharedCharges.TryUseCharge((ent, charges));
if (!emaggedEvent.Repeatable)
{
diff --git a/Content.Shared/Magic/SpellbookSystem.cs b/Content.Shared/Magic/SpellbookSystem.cs
index ce1628bacbf..39fa16f6223 100644
--- a/Content.Shared/Magic/SpellbookSystem.cs
+++ b/Content.Shared/Magic/SpellbookSystem.cs
@@ -1,4 +1,5 @@
using Content.Shared.Actions;
+using Content.Shared.Charges.Systems;
using Content.Shared.DoAfter;
using Content.Shared.Interaction.Events;
using Content.Shared.Magic.Components;
@@ -9,6 +10,7 @@ namespace Content.Shared.Magic;
public sealed class SpellbookSystem : EntitySystem
{
+ [Dependency] private readonly SharedChargesSystem _sharedCharges = default!;
[Dependency] private readonly SharedMindSystem _mind = default!;
[Dependency] private readonly SharedDoAfterSystem _doAfter = default!;
[Dependency] private readonly SharedActionsSystem _actions = default!;
@@ -30,11 +32,7 @@ private void OnInit(Entity ent, ref MapInitEvent args)
if (spell == null)
continue;
- int? charge = charges;
- if (_actions.GetCharges(spell) != null)
- charge = _actions.GetCharges(spell);
-
- _actions.SetCharges(spell, charge < 0 ? null : charge);
+ _sharedCharges.SetCharges(spell.Value, charges);
ent.Comp.Spells.Add(spell.Value);
}
}
@@ -75,7 +73,7 @@ private void OnDoAfter(Entity ent, ref T args) where T :
{
EntityUid? actionId = null;
if (_actions.AddAction(args.Args.User, ref actionId, id))
- _actions.SetCharges(actionId, charges < 0 ? null : charges);
+ _sharedCharges.SetCharges(actionId.Value, charges);
}
}
diff --git a/Content.Shared/Mining/Components/OreVeinComponent.cs b/Content.Shared/Mining/Components/OreVeinComponent.cs
index a26ceaf8915..8f30060bcc2 100644
--- a/Content.Shared/Mining/Components/OreVeinComponent.cs
+++ b/Content.Shared/Mining/Components/OreVeinComponent.cs
@@ -1,6 +1,8 @@
using Content.Shared.Random;
+using Content.Shared.Whitelist;
using Robust.Shared.Prototypes;
-using Content.Shared.Whitelist; // Frontier
+
+// Frontier
namespace Content.Shared.Mining.Components;
diff --git a/Content.Shared/Ninja/Systems/DashAbilitySystem.cs b/Content.Shared/Ninja/Systems/DashAbilitySystem.cs
index f8fad19b131..d4ca761dae0 100644
--- a/Content.Shared/Ninja/Systems/DashAbilitySystem.cs
+++ b/Content.Shared/Ninja/Systems/DashAbilitySystem.cs
@@ -31,7 +31,7 @@ public sealed class DashAbilitySystem : EntitySystem
{
[Dependency] private readonly ActionContainerSystem _actionContainer = default!;
[Dependency] private readonly IGameTiming _timing = default!;
- [Dependency] private readonly SharedChargesSystem _charges = default!;
+ [Dependency] private readonly SharedChargesSystem _sharedCharges = default!;
[Dependency] private readonly SharedHandsSystem _hands = default!;
[Dependency] private readonly ExamineSystemShared _examine = default!;
[Dependency] private readonly SharedPopupSystem _popup = default!;
@@ -88,7 +88,7 @@ private void OnDash(Entity ent, ref DashEvent args)
return;
}
- if (!_charges.TryUseCharge(uid))
+ if (!_sharedCharges.TryUseCharge(uid))
{
_popup.PopupClient(Loc.GetString("dash-ability-no-charges", ("item", uid)), user, user);
return;
diff --git a/Content.Shared/RCD/Components/RCDAmmoComponent.cs b/Content.Shared/RCD/Components/RCDAmmoComponent.cs
index d021ea46e86..8507521c576 100644
--- a/Content.Shared/RCD/Components/RCDAmmoComponent.cs
+++ b/Content.Shared/RCD/Components/RCDAmmoComponent.cs
@@ -11,7 +11,7 @@ public sealed partial class RCDAmmoComponent : Component
/// How many charges are contained in this ammo cartridge.
/// Can be partially transferred into an RCD, until it is empty then it gets deleted.
///
- [DataField("charges"), ViewVariables(VVAccess.ReadWrite), AutoNetworkedField]
+ [DataField, AutoNetworkedField]
public int Charges = 30;
///
diff --git a/Content.Shared/RCD/Systems/RCDAmmoSystem.cs b/Content.Shared/RCD/Systems/RCDAmmoSystem.cs
index b8b483f1ee7..9104591ad11 100644
--- a/Content.Shared/RCD/Systems/RCDAmmoSystem.cs
+++ b/Content.Shared/RCD/Systems/RCDAmmoSystem.cs
@@ -10,7 +10,7 @@ namespace Content.Shared.RCD.Systems;
public sealed class RCDAmmoSystem : EntitySystem
{
- [Dependency] private readonly SharedChargesSystem _charges = default!;
+ [Dependency] private readonly SharedChargesSystem _sharedCharges = default!;
[Dependency] private readonly SharedPopupSystem _popup = default!;
[Dependency] private readonly IGameTiming _timing = default!;
@@ -41,6 +41,7 @@ private void OnAfterInteract(EntityUid uid, RCDAmmoComponent comp, AfterInteract
!TryComp(target, out var charges))
return;
+ var current = _sharedCharges.GetCurrentCharges((target, charges));
var user = args.User;
// ## Frontier - Shipyard RCD ammo only fits in shipyard RCD.
@@ -53,7 +54,7 @@ private void OnAfterInteract(EntityUid uid, RCDAmmoComponent comp, AfterInteract
}
args.Handled = true;
- var count = Math.Min(charges.MaxCharges - charges.Charges, comp.Charges);
+ var count = Math.Min(charges.MaxCharges - current, comp.Charges);
if (count <= 0)
{
_popup.PopupClient(Loc.GetString("rcd-ammo-component-after-interact-full"), target, user);
@@ -61,7 +62,7 @@ private void OnAfterInteract(EntityUid uid, RCDAmmoComponent comp, AfterInteract
}
_popup.PopupClient(Loc.GetString("rcd-ammo-component-after-interact-refilled"), target, user);
- _charges.AddCharges(target, count, charges);
+ _sharedCharges.AddCharges(target, count);
comp.Charges -= count;
Dirty(uid, comp);
diff --git a/Content.Shared/RCD/Systems/RCDSystem.cs b/Content.Shared/RCD/Systems/RCDSystem.cs
index 8875cb43972..114c4f539af 100644
--- a/Content.Shared/RCD/Systems/RCDSystem.cs
+++ b/Content.Shared/RCD/Systems/RCDSystem.cs
@@ -24,7 +24,6 @@
using Content.Shared._NF.Shipyard.Components;
using Content.Shared.Access.Components;
using Content.Shared.Administration.Logs;
-using Content.Shared.Charges.Components;
using Content.Shared.Charges.Systems;
using Content.Shared.Construction;
using Content.Shared.Database;
@@ -49,6 +48,7 @@
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization;
using Robust.Shared.Timing;
+
// Frontier
namespace Content.Shared.RCD.Systems;
@@ -62,7 +62,7 @@ public class RCDSystem : EntitySystem
[Dependency] private readonly ITileDefinitionManager _tileDefMan = default!;
[Dependency] private readonly FloorTileSystem _floors = default!;
[Dependency] private readonly SharedAudioSystem _audio = default!;
- [Dependency] private readonly SharedChargesSystem _charges = default!;
+ [Dependency] private readonly SharedChargesSystem _sharedCharges = default!;
[Dependency] private readonly SharedDoAfterSystem _doAfter = default!;
[Dependency] private readonly SharedInteractionSystem _interaction = default!;
[Dependency] private readonly SharedPopupSystem _popup = default!;
@@ -387,7 +387,7 @@ private void OnDoAfter(EntityUid uid, RCDComponent component, RCDDoAfterEvent ar
// Play audio and consume charges
_audio.PlayPredicted(component.SuccessSound, uid, args.User);
- _charges.UseCharges(uid, args.Cost);
+ _sharedCharges.AddCharges(uid, -args.Cost);
}
private void OnRCDconstructionGhostRotationEvent(RCDConstructionGhostRotationEvent ev, EntitySessionEventArgs session)
@@ -419,11 +419,13 @@ public bool IsRCDOperationStillValid(EntityUid uid, RCDComponent component, MapG
// Update cached prototype if required
UpdateCachedPrototype(uid, component);
+ var prototype = _protoManager.Index(component.ProtoId);
+
// Check that the RCD has enough ammo to get the job done
- TryComp(uid, out var charges);
+ var charges = _sharedCharges.GetCurrentCharges(uid);
// Both of these were messages were suppose to be predicted, but HasInsufficientCharges wasn't being checked on the client for some reason?
- if (_charges.IsEmpty(uid, charges))
+ if (charges == 0)
{
if (popMsgs)
_popup.PopupClient(Loc.GetString("rcd-component-no-ammo-message"), uid, user);
@@ -431,7 +433,7 @@ public bool IsRCDOperationStillValid(EntityUid uid, RCDComponent component, MapG
return false;
}
- if (_charges.HasInsufficientCharges(uid, component.CachedPrototype.Cost, charges))
+ if (prototype.Cost > charges)
{
if (popMsgs)
_popup.PopupClient(Loc.GetString("rcd-component-insufficient-ammo-message"), uid, user);
diff --git a/Content.Shared/SprayPainter/Components/PaintableAirlockComponent.cs b/Content.Shared/SprayPainter/Components/PaintableAirlockComponent.cs
deleted file mode 100644
index fdd0aeeb7f9..00000000000
--- a/Content.Shared/SprayPainter/Components/PaintableAirlockComponent.cs
+++ /dev/null
@@ -1,24 +0,0 @@
-using Content.Shared.Roles;
-using Content.Shared.SprayPainter.Prototypes;
-using Robust.Shared.GameStates;
-using Robust.Shared.Prototypes;
-
-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;
-
- ///
- /// 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;
-}
diff --git a/Content.Shared/SprayPainter/Components/PaintableComponent.cs b/Content.Shared/SprayPainter/Components/PaintableComponent.cs
new file mode 100644
index 00000000000..cfcb6a6c63c
--- /dev/null
+++ b/Content.Shared/SprayPainter/Components/PaintableComponent.cs
@@ -0,0 +1,19 @@
+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;
+}
diff --git a/Content.Shared/SprayPainter/Components/PaintedComponent.cs b/Content.Shared/SprayPainter/Components/PaintedComponent.cs
new file mode 100644
index 00000000000..83f0e6e692c
--- /dev/null
+++ b/Content.Shared/SprayPainter/Components/PaintedComponent.cs
@@ -0,0 +1,18 @@
+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;
+}
diff --git a/Content.Shared/SprayPainter/Components/SprayPainterAmmo.cs b/Content.Shared/SprayPainter/Components/SprayPainterAmmo.cs
new file mode 100644
index 00000000000..d869c96d31c
--- /dev/null
+++ b/Content.Shared/SprayPainter/Components/SprayPainterAmmo.cs
@@ -0,0 +1,17 @@
+using Robust.Shared.GameStates;
+
+namespace Content.Shared.SprayPainter.Components;
+
+///
+/// Items with this component can be used to recharge a spray painter.
+///
+[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
+[Access(typeof(SprayPainterAmmoSystem))]
+public sealed partial class SprayPainterAmmoComponent : Component
+{
+ ///
+ /// The value by which the charge in the spray painter will be recharged.
+ ///
+ [DataField, AutoNetworkedField]
+ public int Charges = 15;
+}
diff --git a/Content.Shared/SprayPainter/Components/SprayPainterComponent.cs b/Content.Shared/SprayPainter/Components/SprayPainterComponent.cs
index 0591cb2dcbd..5485870766d 100644
--- a/Content.Shared/SprayPainter/Components/SprayPainterComponent.cs
+++ b/Content.Shared/SprayPainter/Components/SprayPainterComponent.cs
@@ -1,26 +1,42 @@
-using Content.Shared.DoAfter;
+using Content.Shared.Decals;
using Robust.Shared.Audio;
using Robust.Shared.GameStates;
+using Robust.Shared.Prototypes;
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 +45,82 @@ 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));
+}
+
+///
+/// A set of operating modes for decal painting.
+///
+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,
}
diff --git a/Content.Shared/SprayPainter/Prototypes/AirlockDepartmentsPrototype.cs b/Content.Shared/SprayPainter/Prototypes/AirlockDepartmentsPrototype.cs
deleted file mode 100644
index b61aa037cc9..00000000000
--- a/Content.Shared/SprayPainter/Prototypes/AirlockDepartmentsPrototype.cs
+++ /dev/null
@@ -1,21 +0,0 @@
-using Content.Shared.Roles;
-using Robust.Shared.Prototypes;
-
-namespace Content.Shared.SprayPainter.Prototypes;
-
-///
-/// Maps airlock style names to department ids.
-///
-[Prototype("airlockDepartments")]
-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();
-}
diff --git a/Content.Shared/SprayPainter/Prototypes/AirlockGroupPrototype.cs b/Content.Shared/SprayPainter/Prototypes/AirlockGroupPrototype.cs
deleted file mode 100644
index 24c28b8b7a7..00000000000
--- a/Content.Shared/SprayPainter/Prototypes/AirlockGroupPrototype.cs
+++ /dev/null
@@ -1,19 +0,0 @@
-using Robust.Shared.Prototypes;
-
-namespace Content.Shared.SprayPainter.Prototypes;
-
-[Prototype("AirlockGroup")]
-public sealed partial class AirlockGroupPrototype : IPrototype
-{
- [IdDataField]
- public string ID { get; private set; } = 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;
-}
diff --git a/Content.Shared/SprayPainter/Prototypes/PaintableGroupCategoryPrototype.cs b/Content.Shared/SprayPainter/Prototypes/PaintableGroupCategoryPrototype.cs
new file mode 100644
index 00000000000..ba6423d94a5
--- /dev/null
+++ b/Content.Shared/SprayPainter/Prototypes/PaintableGroupCategoryPrototype.cs
@@ -0,0 +1,19 @@
+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();
+}
diff --git a/Content.Shared/SprayPainter/Prototypes/PaintableGroupPrototype.cs b/Content.Shared/SprayPainter/Prototypes/PaintableGroupPrototype.cs
new file mode 100644
index 00000000000..73944c4e6e3
--- /dev/null
+++ b/Content.Shared/SprayPainter/Prototypes/PaintableGroupPrototype.cs
@@ -0,0 +1,53 @@
+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
+}
diff --git a/Content.Shared/SprayPainter/SharedSprayPainterSystem.cs b/Content.Shared/SprayPainter/SharedSprayPainterSystem.cs
index 48a941d598f..266e3f74d6f 100644
--- a/Content.Shared/SprayPainter/SharedSprayPainterSystem.cs
+++ b/Content.Shared/SprayPainter/SharedSprayPainterSystem.cs
@@ -1,115 +1,177 @@
+using System.Linq;
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.Prototypes;
-using System.Linq;
+using Robust.Shared.Timing;
+using Robust.Shared.Utility;
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 class SharedSprayPainterSystem : EntitySystem
{
+ [Dependency] private readonly IGameTiming _timing = default!;
[Dependency] protected readonly IPrototypeManager Proto = default!;
- [Dependency] private readonly ISharedAdminLogManager _adminLogger = default!;
+ [Dependency] protected readonly ISharedAdminLogManager AdminLogger = default!;
[Dependency] protected readonly SharedAppearanceSystem Appearance = default!;
[Dependency] protected readonly SharedAudioSystem Audio = default!;
+ [Dependency] protected readonly SharedChargesSystem Charges = default!;
[Dependency] protected readonly SharedDoAfterSystem DoAfter = default!;
- [Dependency] private readonly 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 readonly SharedPopupSystem _popup = 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);
+ });
}
private void OnMapInit(Entity ent, ref MapInitEvent args)
{
- if (ent.Comp.ColorPalette.Count == 0)
+ bool stylesByGroupPopulated = false;
+ foreach (var groupProto in Proto.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);
-
+ 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.TryUseCharges(new Entity(ent, EnsureComp(ent)), args.Cost);
- args.Handled = true;
- }
+ var paintedComponent = EnsureComp(target);
+ paintedComponent.DryTime = _timing.CurTime + ent.Comp.FreshPaintDuration;
+ Dirty(target, paintedComponent);
- #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;
}
- 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 +179,140 @@ 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)
+ || !Proto.TryIndex(group, out PaintableGroupPrototype? targetGroup))
+ return;
+
+ // Valid paint target.
+ args.Handled = true;
+
+ if (TryComp(args.Used, out var charges)
+ && charges.LastCharges < targetGroup.Cost)
+ {
+ var msg = Loc.GetString("spray-painter-interact-no-charges");
+ _popup.PopupClient(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");
+ var msg = Loc.GetString("spray-painter-style-not-available");
_popup.PopupClient(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
- private void OnPrototypesReloaded(PrototypesReloadedEventArgs args)
+ #region UI
+
+ ///
+ /// 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);
+ }
+
+ ///
+ /// 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);
diff --git a/Content.Shared/SprayPainter/SprayPainterAmmoSystem.cs b/Content.Shared/SprayPainter/SprayPainterAmmoSystem.cs
new file mode 100644
index 00000000000..d43420efc52
--- /dev/null
+++ b/Content.Shared/SprayPainter/SprayPainterAmmoSystem.cs
@@ -0,0 +1,62 @@
+using Content.Shared.Charges.Components;
+using Content.Shared.Charges.Systems;
+using Content.Shared.Examine;
+using Content.Shared.Interaction;
+using Content.Shared.Popups;
+using Content.Shared.SprayPainter.Components;
+
+namespace Content.Shared.SprayPainter;
+
+///
+/// The system handles interactions with spray painter ammo.
+///
+public sealed class SprayPainterAmmoSystem : EntitySystem
+{
+ [Dependency] private readonly SharedChargesSystem _charges = default!;
+ [Dependency] private readonly SharedPopupSystem _popup = default!;
+
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ SubscribeLocalEvent(OnExamine);
+ SubscribeLocalEvent(OnAfterInteract);
+ }
+
+ private void OnAfterInteract(Entity ent, ref AfterInteractEvent args)
+ {
+ if (args.Handled || !args.CanReach)
+ return;
+
+ if (args.Target is not { Valid: true } target ||
+ !HasComp(target) ||
+ !TryComp(target, out var charges))
+ return;
+
+ var user = args.User;
+ args.Handled = true;
+ var count = Math.Min(charges.MaxCharges - charges.LastCharges, ent.Comp.Charges);
+ if (count <= 0)
+ {
+ _popup.PopupClient(Loc.GetString("spray-painter-ammo-after-interact-full"), target, user);
+ return;
+ }
+
+ _popup.PopupClient(Loc.GetString("spray-painter-ammo-after-interact-refilled"), target, user);
+ _charges.AddCharges(target, count);
+ ent.Comp.Charges -= count;
+ Dirty(ent, ent.Comp);
+
+ if (ent.Comp.Charges <= 0)
+ PredictedQueueDel(ent.Owner);
+ }
+
+ private void OnExamine(Entity ent, ref ExaminedEvent args)
+ {
+ if (!args.IsInDetailsRange)
+ return;
+
+ var examineMessage = Loc.GetString("rcd-ammo-component-on-examine", ("charges", ent.Comp.Charges));
+ args.PushText(examineMessage);
+ }
+}
diff --git a/Content.Shared/SprayPainter/SprayPainterEvents.cs b/Content.Shared/SprayPainter/SprayPainterEvents.cs
index b88b054ad14..db9de9c2787 100644
--- a/Content.Shared/SprayPainter/SprayPainterEvents.cs
+++ b/Content.Shared/SprayPainter/SprayPainterEvents.cs
@@ -1,4 +1,7 @@
+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 +13,75 @@ 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 partial class SprayPainterDoAfterEvent : DoAfterEvent
+{
+ ///
+ /// The prototype to use to repaint this object.
+ ///
+ [DataField]
+ public string Prototype;
+
///
- /// Base RSI path to set for the door sprite.
+ /// The group ID of the object being painted.
///
[DataField]
- public string Sprite;
+ public string Group;
///
- /// Department id to set for the door, if the style has one.
+ /// The cost, in charges, to paint this object.
///
[DataField]
- public string? Department;
+ public int Cost;
- public SprayPainterDoorDoAfterEvent(string sprite, string? department)
+ 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 +103,17 @@ 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);
diff --git a/Content.Shared/_DV/Abilities/CoughingUpItemComponent.cs b/Content.Shared/_DV/Abilities/CoughingUpItemComponent.cs
new file mode 100644
index 00000000000..07a4fa995ff
--- /dev/null
+++ b/Content.Shared/_DV/Abilities/CoughingUpItemComponent.cs
@@ -0,0 +1,19 @@
+using Robust.Shared.GameStates;
+using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
+
+namespace Content.Shared._DV.Abilities;
+
+///
+/// Spawns the item from after the coughing sound is finished.
+///
+///
+/// Client doesn't care about spawning so the field isn't networked.
+///
+[RegisterComponent, NetworkedComponent, Access(typeof(ItemCougherSystem))]
+[AutoGenerateComponentPause]
+public sealed partial class CoughingUpItemComponent : Component
+{
+ [DataField(customTypeSerializer: typeof(TimeOffsetSerializer))]
+ [AutoPausedField]
+ public TimeSpan NextCough;
+}
diff --git a/Content.Shared/_DV/Abilities/ItemCougherComponent.cs b/Content.Shared/_DV/Abilities/ItemCougherComponent.cs
new file mode 100644
index 00000000000..74936f5232b
--- /dev/null
+++ b/Content.Shared/_DV/Abilities/ItemCougherComponent.cs
@@ -0,0 +1,48 @@
+using Robust.Shared.Audio;
+using Robust.Shared.GameStates;
+using Robust.Shared.Prototypes;
+
+namespace Content.Shared._DV.Abilities;
+
+///
+/// Adds an action to cough up an item.
+/// Other systems can enable this action when their conditions are met.
+///
+[RegisterComponent, NetworkedComponent, Access(typeof(ItemCougherSystem))]
+public sealed partial class ItemCougherComponent : Component
+{
+ ///
+ /// The item to spawn after the coughing sound plays.
+ ///
+ [DataField(required: true)]
+ public EntProtoId Item;
+
+ ///
+ /// The action to give the player.
+ ///
+ [DataField(required: true)]
+ public EntProtoId Action;
+
+ [DataField]
+ public EntityUid? ActionEntity;
+
+ ///
+ /// Popup to show to everyone when coughing up an item.
+ /// Gets "name" passed as the identity of the mob.
+ ///
+ [DataField(required: true)]
+ public LocId CoughPopup;
+
+ ///
+ /// Sound played
+ /// The sound length controls how long it takes for the item to spawn.
+ ///
+ [DataField]
+ public SoundSpecifier Sound = new SoundPathSpecifier("/Audio/Animals/cat_hiss.ogg")
+ {
+ Params = new AudioParams
+ {
+ Variation = 0.15f
+ }
+ };
+}
diff --git a/Content.Shared/_DV/Abilities/ItemCougherSystem.cs b/Content.Shared/_DV/Abilities/ItemCougherSystem.cs
new file mode 100644
index 00000000000..2cae7d26275
--- /dev/null
+++ b/Content.Shared/_DV/Abilities/ItemCougherSystem.cs
@@ -0,0 +1,112 @@
+using Content.Shared.Actions;
+using Content.Shared.Clothing.Components;
+using Content.Shared.IdentityManagement;
+using Content.Shared.Inventory;
+using Content.Shared.Popups;
+using Robust.Shared.Audio.Systems;
+using Robust.Shared.Network;
+using Robust.Shared.Timing;
+
+namespace Content.Shared._DV.Abilities;
+
+public sealed class ItemCougherSystem : EntitySystem
+{
+ [Dependency] private readonly IGameTiming _timing = default!;
+ [Dependency] private readonly INetManager _net = default!;
+ [Dependency] private readonly InventorySystem _inventory = default!;
+ [Dependency] private readonly SharedActionsSystem _actions = default!;
+ [Dependency] private readonly SharedAudioSystem _audio = default!;
+ [Dependency] private readonly SharedPopupSystem _popup = default!;
+
+ private EntityQuery _query;
+
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ _query = GetEntityQuery();
+
+ SubscribeLocalEvent(OnMapInit);
+ SubscribeLocalEvent(OnCoughItemAction);
+ }
+
+ public override void Update(float frameTime)
+ {
+ base.Update(frameTime);
+
+ if (_net.IsClient)
+ return;
+
+ var query = EntityQueryEnumerator();
+ while (query.MoveNext(out var uid, out var coughing, out var comp, out var xform))
+ {
+ if (_timing.CurTime < coughing.NextCough)
+ continue;
+
+ var spawned = Spawn(comp.Item, xform.Coordinates);
+ RemCompDeferred(uid, coughing);
+
+ var ev = new ItemCoughedUpEvent(spawned);
+ RaiseLocalEvent(uid, ref ev);
+ }
+ }
+
+ private void OnMapInit(Entity ent, ref MapInitEvent args)
+ {
+ if (ent.Comp.ActionEntity != null)
+ return;
+
+ _actions.AddAction(ent, ref ent.Comp.ActionEntity, ent.Comp.Action);
+ }
+
+ private void OnCoughItemAction(Entity ent, ref CoughItemActionEvent args)
+ {
+ if (_inventory.TryGetSlotEntity(ent, "mask", out var maskUid) &&
+ TryComp(maskUid, out var mask) &&
+ !mask.IsToggled)
+ {
+ _popup.PopupClient(Loc.GetString("item-cougher-mask", ("mask", maskUid)), ent, ent);
+ return;
+ }
+
+ var msg = Loc.GetString(ent.Comp.CoughPopup, ("name", Identity.Entity(ent, EntityManager)));
+ _popup.PopupPredicted(msg, ent, ent);
+ _audio.PlayPredicted(ent.Comp.Sound, ent, ent);
+
+ var path = _audio.ResolveSound(ent.Comp.Sound); // Frontier: resolve sound
+ var coughing = EnsureComp(ent);
+ coughing.NextCough = _timing.CurTime + _audio.GetAudioLength(path);
+ args.Handled = true;
+
+ // disable it until another system calls EnableAction
+ SetActionEnabled((ent, ent.Comp), false);
+ }
+
+ ///
+ /// Enables the coughing action.
+ /// Other systems have to call this, this is not used internally.
+ ///
+ public void EnableAction(Entity ent)
+ {
+ SetActionEnabled(ent, true);
+ }
+
+ public void SetActionEnabled(Entity ent, bool enabled)
+ {
+ if (!_query.Resolve(ent, ref ent.Comp) || ent.Comp.ActionEntity is not {} action)
+ return;
+
+ _actions.SetEnabled(action, enabled);
+ }
+}
+
+///
+/// Raised on the mob after it coughs up an item.
+///
+[ByRefEvent]
+public record struct ItemCoughedUpEvent(EntityUid Item);
+
+///
+/// Action event that must use.
+///
+public sealed partial class CoughItemActionEvent : InstantActionEvent;
diff --git a/Content.Shared/_DV/Felinid/FelinidComponent.cs b/Content.Shared/_DV/Felinid/FelinidComponent.cs
new file mode 100644
index 00000000000..2aa6f8994a5
--- /dev/null
+++ b/Content.Shared/_DV/Felinid/FelinidComponent.cs
@@ -0,0 +1,20 @@
+using Content.Shared.FixedPoint;
+using Robust.Shared.GameStates;
+
+namespace Content.Shared._DV.Abilities.Felinid;
+
+///
+/// Felenid god component controls 3 things:
+/// 1. When you use to cough up a hairball, it purges chemicals from your bloodstream.
+/// 2. Enables the cough hairball action after eating a mouse with FelinidFoodComponent.
+/// 3. Full immunity to hairball vomiting chance.
+///
+[RegisterComponent, NetworkedComponent, Access(typeof(SharedFelinidSystem))]
+public sealed partial class FelinidComponent : Component
+{
+ ///
+ /// Quantity of reagents to purge from the bloodstream.
+ ///
+ [DataField]
+ public FixedPoint2 PurgedQuantity = 20;
+}
diff --git a/Content.Shared/_DV/Felinid/FelinidFoodComponent.cs b/Content.Shared/_DV/Felinid/FelinidFoodComponent.cs
new file mode 100644
index 00000000000..b81573dd1ce
--- /dev/null
+++ b/Content.Shared/_DV/Felinid/FelinidFoodComponent.cs
@@ -0,0 +1,16 @@
+using Robust.Shared.GameStates;
+
+namespace Content.Shared._DV.Abilities.Felinid;
+
+///
+/// Makes this food let felinids cough up a hairball when eaten.
+///
+[RegisterComponent, NetworkedComponent, Access(typeof(SharedFelinidSystem))]
+public sealed partial class FelinidFoodComponent : Component
+{
+ ///
+ /// Extra hunger to satiate for felinids.
+ ///
+ [DataField]
+ public float BonusHunger = 50f;
+}
diff --git a/Content.Shared/_DV/Felinid/HairballComponent.cs b/Content.Shared/_DV/Felinid/HairballComponent.cs
new file mode 100644
index 00000000000..fa8b4c0726f
--- /dev/null
+++ b/Content.Shared/_DV/Felinid/HairballComponent.cs
@@ -0,0 +1,22 @@
+using Robust.Shared.GameStates;
+
+namespace Content.Shared._DV.Abilities.Felinid;
+
+///
+/// Causes players to randomly vomit when trying to pick this up, or when it gets thrown at them.
+///
+[RegisterComponent, NetworkedComponent, Access(typeof(SharedFelinidSystem))]
+public sealed partial class HairballComponent : Component
+{
+ ///
+ /// The solution to put purged chemicals into.
+ ///
+ [DataField]
+ public string SolutionName = "hairball";
+
+ ///
+ /// Probability of someone vomiting when picking it up or getting it thrown at them.
+ ///
+ [DataField]
+ public float VomitProb = 0.2f;
+}
diff --git a/Content.Shared/_DV/Felinid/SharedFelinidSystem.cs b/Content.Shared/_DV/Felinid/SharedFelinidSystem.cs
new file mode 100644
index 00000000000..9652925f3c4
--- /dev/null
+++ b/Content.Shared/_DV/Felinid/SharedFelinidSystem.cs
@@ -0,0 +1,32 @@
+using Content.Shared.Nutrition;
+using Content.Shared.Nutrition.Components;
+using Content.Shared.Nutrition.EntitySystems;
+
+namespace Content.Shared._DV.Abilities.Felinid;
+
+///
+/// Makes eating enable a felinids hairball action.
+/// Other interactions are in the server system.
+///
+public abstract class SharedFelinidSystem : EntitySystem
+{
+ [Dependency] private readonly HungerSystem _hunger = default!;
+ [Dependency] private readonly ItemCougherSystem _cougher = default!;
+
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ SubscribeLocalEvent(OnMouseEaten);
+ }
+
+ private void OnMouseEaten(Entity ent, ref BeforeFullyEatenEvent args)
+ {
+ var user = args.User;
+ if (!HasComp(user) || !TryComp(user, out var hunger))
+ return;
+
+ _hunger.ModifyHunger(user, ent.Comp.BonusHunger, hunger);
+ _cougher.EnableAction(user);
+ }
+}
diff --git a/Content.Shared/_Null/Systems/SharedMiningSystem.cs b/Content.Shared/_Null/Systems/SharedMiningSystem.cs
new file mode 100644
index 00000000000..6f1c2a86bbd
--- /dev/null
+++ b/Content.Shared/_Null/Systems/SharedMiningSystem.cs
@@ -0,0 +1,3 @@
+namespace Content.Shared._Null.Systems;
+
+public abstract class SharedMiningSystem : EntitySystem;
diff --git a/Resources/Audio/Effects/spray2.ogg b/Resources/Audio/Effects/spray2.ogg
index d184d76bd00..f1a04ddac83 100644
Binary files a/Resources/Audio/Effects/spray2.ogg and b/Resources/Audio/Effects/spray2.ogg differ
diff --git a/Resources/Audio/MidiCustom/space-station-14.sf2 b/Resources/Audio/MidiCustom/space-station-14.sf2
index 54c6eb61963..758567bdbcc 100644
Binary files a/Resources/Audio/MidiCustom/space-station-14.sf2 and b/Resources/Audio/MidiCustom/space-station-14.sf2 differ
diff --git a/Resources/Locale/en-US/_DV/abilities/item-cougher.ftl b/Resources/Locale/en-US/_DV/abilities/item-cougher.ftl
new file mode 100644
index 00000000000..56d80b02f2f
--- /dev/null
+++ b/Resources/Locale/en-US/_DV/abilities/item-cougher.ftl
@@ -0,0 +1 @@
+item-cougher-mask = Take off your {$mask} first.
diff --git a/Resources/Locale/en-US/_Null/warper.ftl b/Resources/Locale/en-US/_Null/warper.ftl
new file mode 100644
index 00000000000..3676106908d
--- /dev/null
+++ b/Resources/Locale/en-US/_Null/warper.ftl
@@ -0,0 +1,7 @@
+warper-cancelled-sealed = This warper appears sealed, for now.
+warper-cancelled-no-destination = This warper has no destination.
+warper-cancelled-no-source = This warper has no ID that marks it a valid destination.
+warper-cancelled-invalid-destination = This warper's destination is invalid.
+warper-map-invalid = This warper leads to a harrowing void. Perhaps it's best I should not enter...
+warper-on-examine-source = This is labelled ID "{$location}".
+warper-on-examine-destination = This warper leads to location ID "{$location}".
\ No newline at end of file
diff --git a/Resources/Locale/en-US/engineer-painter/engineer-painter.ftl b/Resources/Locale/en-US/engineer-painter/engineer-painter.ftl
deleted file mode 100644
index d3d3ccc4448..00000000000
--- a/Resources/Locale/en-US/engineer-painter/engineer-painter.ftl
+++ /dev/null
@@ -1,14 +0,0 @@
-spray-painter-window-title = Spray painter
-
-spray-painter-style-not-available = Cannot apply the selected style to this type of airlock
-spray-painter-selected-style = Selected style:
-
-spray-painter-selected-color = Selected color:
-spray-painter-color-red = red
-spray-painter-color-yellow = yellow
-spray-painter-color-brown = brown
-spray-painter-color-green = green
-spray-painter-color-cyan = cyan
-spray-painter-color-blue = blue
-spray-painter-color-white = white
-spray-painter-color-black = black
diff --git a/Resources/Locale/en-US/spray-painter/spray-painter.ftl b/Resources/Locale/en-US/spray-painter/spray-painter.ftl
new file mode 100644
index 00000000000..dc54c5c8b8c
--- /dev/null
+++ b/Resources/Locale/en-US/spray-painter/spray-painter.ftl
@@ -0,0 +1,194 @@
+# Components
+spray-painter-ammo-on-examine = It holds {$charges} charges.
+spray-painter-ammo-after-interact-full = The spray painter is full!
+spray-painter-ammo-after-interact-refilled = You refill the spray painter.
+
+spray-painter-interact-no-charges = Not enough paint left.
+spray-painter-interact-nothing-to-remove = Nothing to remove!
+
+spray-painter-on-examined-painted-message = It seems to have been freshly painted.
+spray-painter-style-not-available = Cannot apply the selected style to this object.
+
+spray-painter-verb-toggle-decals = Toggle decal painting
+
+spray-painter-item-status-label = Decals: {$mode}
+spray-painter-item-status-add = [color=green]Add[/color]
+spray-painter-item-status-remove = [color=red]Remove[/color]
+spray-painter-item-status-off = [color=gray]Off[/color]
+
+# UI
+spray-painter-window-title = Spray Painter
+
+spray-painter-selected-style = Selected style:
+
+spray-painter-selected-decals = Selected decal:
+spray-painter-use-custom-color = Use custom color
+spray-painter-use-snap-to-tile = Snap to tile
+
+spray-painter-angle-rotation = 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 = Selected color:
+spray-painter-color-red = red
+spray-painter-color-yellow = yellow
+spray-painter-color-brown = brown
+spray-painter-color-green = green
+spray-painter-color-cyan = cyan
+spray-painter-color-blue = blue
+spray-painter-color-white = white
+spray-painter-color-black = black
+
+# Categories (tabs)
+spray-painter-tab-category-airlocks = Airlocks
+spray-painter-tab-category-canisters = Canisters
+spray-painter-tab-category-crates = Crates
+spray-painter-tab-category-lockers = Lockers
+spray-painter-tab-category-pipes = Pipes
+spray-painter-tab-category-decals = Decals
+
+# Groups (subtabs)
+spray-painter-tab-group-airlockstandard = Standard
+spray-painter-tab-group-airlockglass = Glass
+
+spray-painter-tab-group-cratesteel = Steel
+spray-painter-tab-group-crateplastic = Plastic
+spray-painter-tab-group-cratesecure = Secure
+
+spray-painter-tab-group-closet = Unlocked
+spray-painter-tab-group-locker = Secure
+spray-painter-tab-group-wallcloset = Unlocked (Wall)
+spray-painter-tab-group-walllocker = Secure (Wall)
+
+# Airlocks
+spray-painter-style-airlockstandard-atmospherics = Atmospheric
+spray-painter-style-airlockstandard-basic = Basic
+spray-painter-style-airlockstandard-cargo = Cargo
+spray-painter-style-airlockstandard-chemistry = Chemistry
+spray-painter-style-airlockstandard-command = Command
+spray-painter-style-airlockstandard-engineering = Engineering
+spray-painter-style-airlockstandard-freezer = Freezer
+spray-painter-style-airlockstandard-hydroponics = Hydroponics
+spray-painter-style-airlockstandard-maintenance = Maintenance
+spray-painter-style-airlockstandard-medical = Medical
+spray-painter-style-airlockstandard-salvage = Salvage
+spray-painter-style-airlockstandard-science = Science
+spray-painter-style-airlockstandard-security = Security
+spray-painter-style-airlockstandard-virology = Virology
+
+spray-painter-style-airlockglass-atmospherics = Atmospherics
+spray-painter-style-airlockglass-basic = Basic
+spray-painter-style-airlockglass-cargo = Cargo
+spray-painter-style-airlockglass-chemistry = Chemistry
+spray-painter-style-airlockglass-command = Command
+spray-painter-style-airlockglass-engineering = Engineering
+spray-painter-style-airlockglass-hydroponics = Hydroponics
+spray-painter-style-airlockglass-maintenance = Maintenance
+spray-painter-style-airlockglass-medical = Medical
+spray-painter-style-airlockglass-salvage = Salvage
+spray-painter-style-airlockglass-science = Science
+spray-painter-style-airlockglass-security = Security
+spray-painter-style-airlockglass-virology = Virology
+
+# Lockers
+spray-painter-style-locker-atmospherics = Atmospherics
+spray-painter-style-locker-basic = Basic
+spray-painter-style-locker-botanist = Botanist
+spray-painter-style-locker-brigmedic = Brigmedic
+spray-painter-style-locker-captain = Captain
+spray-painter-style-locker-ce = CE
+spray-painter-style-locker-chemical = Chemical
+spray-painter-style-locker-clown = Clown
+spray-painter-style-locker-cmo = CMO
+spray-painter-style-locker-doctor = Doctor
+spray-painter-style-locker-electrical = Electrical
+spray-painter-style-locker-engineer = Engineer
+spray-painter-style-locker-evac = Evac repair
+spray-painter-style-locker-hop = HOP
+spray-painter-style-locker-hos = HOS
+spray-painter-style-locker-medicine = Medicine
+spray-painter-style-locker-mime = Mime
+spray-painter-style-locker-paramedic = Paramedic
+spray-painter-style-locker-quartermaster = Quartermaster
+spray-painter-style-locker-rd = RD
+spray-painter-style-locker-representative = Representative
+spray-painter-style-locker-salvage = Salvage
+spray-painter-style-locker-scientist = Scientist
+spray-painter-style-locker-security = Security
+spray-painter-style-locker-welding = Welding
+
+spray-painter-style-closet-basic = Basic
+spray-painter-style-closet-biohazard = Biohazard
+spray-painter-style-closet-biohazard-science = Biohazard (science)
+spray-painter-style-closet-biohazard-virology = Biohazard (virology)
+spray-painter-style-closet-biohazard-security = Biohazard (security)
+spray-painter-style-closet-biohazard-janitor = Biohazard (janitor)
+spray-painter-style-closet-bomb = Bomb suit
+spray-painter-style-closet-bomb-janitor = Bomb suit (janitor)
+spray-painter-style-closet-chef = Chef
+spray-painter-style-closet-fire = Fire-safety
+spray-painter-style-closet-janitor = Janitor
+spray-painter-style-closet-legal = Lawyer
+spray-painter-style-closet-nitrogen = Internals (nitrogen)
+spray-painter-style-closet-oxygen = Internals (oxygen)
+spray-painter-style-closet-radiation = Radiation suit
+spray-painter-style-closet-tool = Tools
+
+spray-painter-style-wallcloset-atmospherics = Atmospherics
+spray-painter-style-wallcloset-basic = Basic
+spray-painter-style-wallcloset-black = Black
+spray-painter-style-wallcloset-blue = Blue
+spray-painter-style-wallcloset-fire = Fire-safety
+spray-painter-style-wallcloset-green = Green
+spray-painter-style-wallcloset-grey = Grey
+spray-painter-style-wallcloset-mixed = Mixed
+spray-painter-style-wallcloset-nitrogen = Internals (nitrogen)
+spray-painter-style-wallcloset-orange = Orange
+spray-painter-style-wallcloset-oxygen = Internals (oxygen)
+spray-painter-style-wallcloset-pink = Pink
+spray-painter-style-wallcloset-white = White
+spray-painter-style-wallcloset-yellow = Yellow
+
+spray-painter-style-walllocker-evac = Evac repair
+spray-painter-style-walllocker-medical = Medical
+
+# Crates
+spray-painter-style-cratesteel-basic = Basic
+spray-painter-style-cratesteel-electrical = Electrical
+spray-painter-style-cratesteel-engineering = Engineering
+spray-painter-style-cratesteel-radiation = Radiation
+spray-painter-style-cratesteel-science = Science
+spray-painter-style-cratesteel-surgery = Surgery
+
+spray-painter-style-crateplastic-basic = Basic
+spray-painter-style-crateplastic-chemistry = Chemistry
+spray-painter-style-crateplastic-command = Command
+spray-painter-style-crateplastic-hydroponics = Hydroponics
+spray-painter-style-crateplastic-medical = Medical
+spray-painter-style-crateplastic-oxygen = Oxygen
+
+spray-painter-style-cratesecure-basic = Basic
+spray-painter-style-cratesecure-chemistry = Chemistry
+spray-painter-style-cratesecure-command = Command
+spray-painter-style-cratesecure-engineering = Engineering
+spray-painter-style-cratesecure-hydroponics = Hydroponics
+spray-painter-style-cratesecure-medical = Medical
+spray-painter-style-cratesecure-plasma = Plasma
+spray-painter-style-cratesecure-private = Private
+spray-painter-style-cratesecure-science = Science
+spray-painter-style-cratesecure-secgear = Secgear
+spray-painter-style-cratesecure-weapon = Weapon
+
+# Canisters
+spray-painter-style-canisters-air = Air
+spray-painter-style-canisters-ammonia = Ammonia
+spray-painter-style-canisters-carbon-dioxide = Carbon dioxide
+spray-painter-style-canisters-frezon = Frezon
+spray-painter-style-canisters-nitrogen = Nitrogen
+spray-painter-style-canisters-nitrous-oxide = Nitrous oxide
+spray-painter-style-canisters-oxygen = Oxygen
+spray-painter-style-canisters-plasma = Plasma
+spray-painter-style-canisters-storage = Storage
+spray-painter-style-canisters-tritium = Tritium
+spray-painter-style-canisters-water-vapor = Water vapor
diff --git a/Resources/Prototypes/Actions/types.yml b/Resources/Prototypes/Actions/types.yml
index 48a200c398c..acf0fbdf9bf 100644
--- a/Resources/Prototypes/Actions/types.yml
+++ b/Resources/Prototypes/Actions/types.yml
@@ -93,8 +93,9 @@
name: Break Free
description: Activating your freedom implant will free you from any hand restraints
components:
+ - type: LimitedCharges
+ maxCharges: 3
- type: InstantAction
- charges: 3
checkCanInteract: false
itemIconStyle: BigAction
priority: -20
@@ -121,9 +122,10 @@
name: Activate EMP
description: Triggers a small EMP pulse around you
components:
+ - type: LimitedCharges
+ maxCharges: 3
- type: InstantAction
checkCanInteract: false
- charges: 3
useDelay: 5
itemIconStyle: BigAction
priority: -20
@@ -137,9 +139,10 @@
name: SCRAM!
description: Randomly teleports you within a large distance.
components:
+ - type: LimitedCharges
+ maxCharges: 2
- type: InstantAction
checkCanInteract: false
- charges: 2
useDelay: 5
itemIconStyle: BigAction
priority: -20
@@ -155,8 +158,9 @@
components:
- type: ConfirmableAction
popup: dna-scrambler-action-popup
+ - type: LimitedCharges
+ maxCharges: 1
- type: InstantAction
- charges: 1
itemIconStyle: BigAction
priority: -20
icon:
diff --git a/Resources/Prototypes/Catalog/VendingMachines/Inventories/youtool.yml b/Resources/Prototypes/Catalog/VendingMachines/Inventories/youtool.yml
index 0f16e366c09..ad041303dff 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: 5
# 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/Consumable/Food/frozen.yml b/Resources/Prototypes/Entities/Objects/Consumable/Food/frozen.yml
index 1551bde65cf..8b3f08ca695 100644
--- a/Resources/Prototypes/Entities/Objects/Consumable/Food/frozen.yml
+++ b/Resources/Prototypes/Entities/Objects/Consumable/Food/frozen.yml
@@ -293,3 +293,5 @@
- Trash
- type: SpaceGarbage
- type: GoblinPreciousTrash # Frontier
+ - type: StaticPrice
+ price: 1
\ No newline at end of file
diff --git a/Resources/Prototypes/Entities/Objects/Misc/fluff_lights.yml b/Resources/Prototypes/Entities/Objects/Misc/fluff_lights.yml
index 27888fea726..0f263772de2 100644
--- a/Resources/Prototypes/Entities/Objects/Misc/fluff_lights.yml
+++ b/Resources/Prototypes/Entities/Objects/Misc/fluff_lights.yml
@@ -159,7 +159,6 @@
- type: Flash
- type: LimitedCharges
maxCharges: 3
- charges: 3
- type: AutoRecharge
rechargeDuration: 30
- type: MeleeWeapon
diff --git a/Resources/Prototypes/Entities/Objects/Tools/lantern.yml b/Resources/Prototypes/Entities/Objects/Tools/lantern.yml
index 09f8607ef6d..18f7a5d134c 100644
--- a/Resources/Prototypes/Entities/Objects/Tools/lantern.yml
+++ b/Resources/Prototypes/Entities/Objects/Tools/lantern.yml
@@ -88,7 +88,6 @@
- type: Flash
- type: LimitedCharges
maxCharges: 15
- charges: 15
- type: MeleeWeapon
damage:
types:
diff --git a/Resources/Prototypes/Entities/Objects/Tools/spray_painter.yml b/Resources/Prototypes/Entities/Objects/Tools/spray_painter.yml
index d2154326783..23d7b68d37d 100644
--- a/Resources/Prototypes/Entities/Objects/Tools/spray_painter.yml
+++ b/Resources/Prototypes/Entities/Objects/Tools/spray_painter.yml
@@ -1,8 +1,8 @@
- type: entity
- parent: [BaseItem, RecyclableItemDeviceSmall] # Frontier: added RecyclableItemDeviceSmall
+ parent: BaseItem
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
@@ -31,4 +31,46 @@
air: '#03fcd3'
mix: '#947507'
- type: StaticPrice
- price: 20 # Frontier 40<20
+ price: 40
+ - type: LimitedCharges
+ maxCharges: 15
+ lastCharges: 15
+ - type: PhysicalComposition
+ materialComposition:
+ Steel: 100
+
+- type: entity
+ parent: SprayPainter
+ id: SprayPainterRecharging
+ suffix: Admeme
+ components:
+ - type: AutoRecharge
+ rechargeDuration: 1
+
+- type: entity
+ parent: SprayPainter
+ id: SprayPainterEmpty
+ suffix: Empty
+ components:
+ - type: LimitedCharges
+ lastCharges: -1
+
+- type: entity
+ parent: BaseItem
+ id: SprayPainterAmmo
+ name: compressed paint
+ description: A cartridge of highly compressed paint, commonly used in spray painters.
+ components:
+ - type: SprayPainterAmmo
+ - 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
diff --git a/Resources/Prototypes/Entities/Objects/Tools/tools.yml b/Resources/Prototypes/Entities/Objects/Tools/tools.yml
index 304a8bb728d..16236e34c52 100644
--- a/Resources/Prototypes/Entities/Objects/Tools/tools.yml
+++ b/Resources/Prototypes/Entities/Objects/Tools/tools.yml
@@ -378,7 +378,6 @@
- Deconstruct
- type: LimitedCharges
maxCharges: 30
- charges: 30
- type: Sprite
sprite: _NF/Objects/Tools/rcd.rsi # Frontier
state: icon
@@ -410,7 +409,7 @@
suffix: Empty
components:
- type: LimitedCharges
- charges: 0
+ lastCharges: -1
- type: entity
id: RCDRecharging
@@ -421,7 +420,6 @@
components:
- type: LimitedCharges
maxCharges: 20
- charges: 20
- type: AutoRecharge
rechargeDuration: 10
diff --git a/Resources/Prototypes/Entities/Objects/Weapons/Guns/Projectiles/projectiles.yml b/Resources/Prototypes/Entities/Objects/Weapons/Guns/Projectiles/projectiles.yml
index 13916137bc2..3b16f0c87e1 100644
--- a/Resources/Prototypes/Entities/Objects/Weapons/Guns/Projectiles/projectiles.yml
+++ b/Resources/Prototypes/Entities/Objects/Weapons/Guns/Projectiles/projectiles.yml
@@ -1089,6 +1089,7 @@
bounds: "-0.15,-0.3,0.15,0.3"
hard: false
mask:
+ - Opaque
- Impassable
- BulletImpassable
fly-by: *flybyfixture
diff --git a/Resources/Prototypes/Entities/Objects/Weapons/Melee/sword.yml b/Resources/Prototypes/Entities/Objects/Weapons/Melee/sword.yml
index 5b5d6ec020b..c9761c0250f 100644
--- a/Resources/Prototypes/Entities/Objects/Weapons/Melee/sword.yml
+++ b/Resources/Prototypes/Entities/Objects/Weapons/Melee/sword.yml
@@ -104,7 +104,6 @@
- type: DashAbility
- type: LimitedCharges
maxCharges: 3
- charges: 3
- type: AutoRecharge
rechargeDuration: 20
- type: Clothing
diff --git a/Resources/Prototypes/Entities/Objects/Weapons/security.yml b/Resources/Prototypes/Entities/Objects/Weapons/security.yml
index 0b090563ac0..feb5d449648 100644
--- a/Resources/Prototypes/Entities/Objects/Weapons/security.yml
+++ b/Resources/Prototypes/Entities/Objects/Weapons/security.yml
@@ -245,7 +245,6 @@
- type: Flash
- type: LimitedCharges
maxCharges: 5
- charges: 5
- type: MeleeWeapon
wideAnimationRotation: 180
damage:
@@ -281,7 +280,6 @@
components:
- type: LimitedCharges
maxCharges: 2
- charges: 2
- type: entity
name: portable flasher
diff --git a/Resources/Prototypes/Entities/Structures/Doors/Airlocks/airlocks.yml b/Resources/Prototypes/Entities/Structures/Doors/Airlocks/airlocks.yml
index cf6d5a89dff..093b12c1fad 100644
--- a/Resources/Prototypes/Entities/Structures/Doors/Airlocks/airlocks.yml
+++ b/Resources/Prototypes/Entities/Structures/Doors/Airlocks/airlocks.yml
@@ -15,8 +15,7 @@
components:
- type: Sprite
sprite: Structures/Doors/Airlocks/Standard/engineering.rsi
- - type: PaintableAirlock
- department: Engineering
+
- type: Wires
layoutId: AirlockEngineering
@@ -35,8 +34,16 @@
components:
- type: Sprite
sprite: Structures/Doors/Airlocks/Standard/cargo.rsi
- - type: PaintableAirlock
- department: Cargo
+ - type: Wires
+ layoutId: AirlockCargo
+
+- type: entity
+ parent: Airlock
+ id: AirlockSalvage
+ suffix: Salvage
+ components:
+ - type: Sprite
+ sprite: Structures/Doors/Airlocks/Standard/cargo.rsi # Null Sector set to Cargo (we aint got these!)
- type: Wires
layoutId: AirlockCargo
@@ -57,8 +64,6 @@
components:
- type: Sprite
sprite: Structures/Doors/Airlocks/Standard/medical.rsi
- - type: PaintableAirlock
- department: Medical
- type: Wires
layoutId: AirlockMedical
@@ -85,8 +90,6 @@
components:
- type: Sprite
sprite: Structures/Doors/Airlocks/Standard/science.rsi
- - type: PaintableAirlock
- department: Science
- type: Wires
layoutId: AirlockScience
@@ -99,8 +102,6 @@
sprite: Structures/Doors/Airlocks/Standard/command.rsi
- type: WiresPanelSecurity
securityLevel: medSecurity
- - type: PaintableAirlock
- department: Command
- type: Wires
layoutId: AirlockCommand
@@ -111,8 +112,6 @@
components:
- type: Sprite
sprite: Structures/Doors/Airlocks/Standard/security.rsi
- - type: PaintableAirlock
- department: Security
- type: Wires
layoutId: AirlockSecurity
@@ -141,6 +140,8 @@
sprite: Structures/Doors/Airlocks/Standard/mining.rsi
- type: Wires
layoutId: AirlockCargo
+ - type: Paintable
+ group: null
- type: entity
parent: AirlockCommand # if you get centcom door somehow it counts as command, also inherit panel
@@ -157,6 +158,8 @@
components:
- type: Sprite
sprite: Structures/Doors/Airlocks/Standard/hatch.rsi
+ - type: Paintable
+ group: null
- type: entity
parent: Airlock
@@ -165,6 +168,8 @@
components:
- type: Sprite
sprite: Structures/Doors/Airlocks/Standard/hatch_maint.rsi
+ - type: Paintable
+ group: null
# Glass
- type: entity
@@ -174,8 +179,6 @@
components:
- type: Sprite
sprite: Structures/Doors/Airlocks/Glass/engineering.rsi
- - type: PaintableAirlock
- department: Engineering
- type: Wires
layoutId: AirlockEngineering
@@ -202,8 +205,16 @@
components:
- type: Sprite
sprite: Structures/Doors/Airlocks/Glass/cargo.rsi
- - type: PaintableAirlock
- department: Cargo
+ - type: Wires
+ layoutId: AirlockCargo
+
+- type: entity
+ parent: AirlockGlass
+ id: AirlockSalvageGlass
+ suffix: Salvage
+ components:
+ - type: Sprite
+ sprite: Structures/Doors/Airlocks/Glass/cargo.rsi # Null Sector set to Cargo (we aint got these!)
- type: Wires
layoutId: AirlockCargo
@@ -224,8 +235,6 @@
components:
- type: Sprite
sprite: Structures/Doors/Airlocks/Glass/medical.rsi
- - type: PaintableAirlock
- department: Medical
- type: Wires
layoutId: AirlockMedical
@@ -252,8 +261,6 @@
components:
- type: Sprite
sprite: Structures/Doors/Airlocks/Glass/science.rsi
- - type: PaintableAirlock
- department: Science
- type: Wires
layoutId: AirlockScience
@@ -264,8 +271,6 @@
components:
- type: Sprite
sprite: Structures/Doors/Airlocks/Glass/command.rsi
- - type: PaintableAirlock
- department: Command
- type: WiresPanelSecurity
securityLevel: medSecurity
- type: Wires
@@ -278,8 +283,6 @@
components:
- type: Sprite
sprite: Structures/Doors/Airlocks/Glass/security.rsi
- - type: PaintableAirlock
- department: Security
- type: Wires
layoutId: AirlockSecurity
@@ -298,6 +301,8 @@
components:
- type: Sprite
sprite: Structures/Doors/Airlocks/Glass/mining.rsi
+ - type: Paintable
+ group: null
- type: entity
parent: AirlockCommandGlass # see standard
@@ -306,3 +311,31 @@
components:
- type: Sprite
sprite: Structures/Doors/Airlocks/Glass/centcomm.rsi
+
+- type: entity
+ parent: AirlockGlass
+ id: AirlockStandardGlass
+ suffix: Service
+ components:
+ - type: Sprite
+ sprite: Structures/Doors/Airlocks/Glass/basic.rsi
+
+- type: entity
+ parent: Airlock
+ id: AirlockXeno
+ suffix: Xeno
+ components:
+ - type: Sprite
+ sprite: Structures/Doors/Airlocks/Standard/centcomm.rsi # Null Sector : No Xeno Airlocks Implemented
+ - type: Paintable
+ group: null
+
+- type: entity
+ parent: AirlockGlass
+ id: AirlockGlassXeno
+ suffix: Xeno
+ components:
+ - type: Sprite
+ sprite: Structures/Doors/Airlocks/Glass/centcomm.rsi # Null Sector : No Xeno Airlocks Implemented
+ - type: Paintable
+ group: null
diff --git a/Resources/Prototypes/Entities/Structures/Doors/Airlocks/base_structureairlocks.yml b/Resources/Prototypes/Entities/Structures/Doors/Airlocks/base_structureairlocks.yml
index 42e15a4f677..799e1c41381 100644
--- a/Resources/Prototypes/Entities/Structures/Doors/Airlocks/base_structureairlocks.yml
+++ b/Resources/Prototypes/Entities/Structures/Doors/Airlocks/base_structureairlocks.yml
@@ -162,9 +162,8 @@
- type: IconSmooth
key: walls
mode: NoSprite
- - type: PaintableAirlock
- group: Standard
- department: Civilian
+ - type: Paintable
+ group: AirlockStandard
- type: StaticPrice
price: 150
- type: LightningTarget
@@ -225,8 +224,8 @@
- type: Construction
graph: Airlock
node: glassAirlock
- - type: PaintableAirlock
- group: Glass
+ - type: Paintable
+ group: AirlockGlass
- 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 87b76fb16c2..123f1ef2c7f 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
+ group: null
- type: entity
parent: AirlockGlass
@@ -23,5 +25,7 @@
node: glassAirlock
containers:
- board
- # - type: StaticPrice # Frontier - TODO: material value rework
- # price: 165 # Frontier
+ - type: StaticPrice
+ price: 165
+ - type: Paintable
+ group: null
diff --git a/Resources/Prototypes/Entities/Structures/Doors/Airlocks/external.yml b/Resources/Prototypes/Entities/Structures/Doors/Airlocks/external.yml
index 7f5c0190250..773065bea04 100644
--- a/Resources/Prototypes/Entities/Structures/Doors/Airlocks/external.yml
+++ b/Resources/Prototypes/Entities/Structures/Doors/Airlocks/external.yml
@@ -16,11 +16,10 @@
path: /Audio/Machines/airlock_deny.ogg
- type: Sprite
sprite: Structures/Doors/Airlocks/Standard/external.rsi
- - type: PaintableAirlock
- group: External
- department: null
- type: Wires
layoutId: AirlockExternal
+ - type: Paintable
+ group: null
- type: entity
parent: AirlockExternal
@@ -33,8 +32,6 @@
enabled: false
- type: Sprite
sprite: Structures/Doors/Airlocks/Glass/external.rsi
- - type: PaintableAirlock
- group: ExternalGlass
- 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 6c0e4bbc032..8b99f4d24d3 100644
--- a/Resources/Prototypes/Entities/Structures/Doors/Airlocks/shuttle.yml
+++ b/Resources/Prototypes/Entities/Structures/Doors/Airlocks/shuttle.yml
@@ -53,14 +53,13 @@
- type: Tag
tags:
- ForceNoFixRotations
- - type: PaintableAirlock
- group: Shuttle
- department: null
- type: Construction
graph: AirlockShuttle
node: airlock
- type: StaticPrice
price: 350
+ - type: Paintable
+ group: null
- type: entity
id: AirlockGlassShuttle
@@ -73,8 +72,6 @@
sprite: Structures/Doors/Airlocks/Glass/shuttle.rsi
- type: Occluder
enabled: false
- - type: PaintableAirlock
- group: ShuttleGlass
- 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
deleted file mode 100644
index 23d17f008d4..00000000000
--- a/Resources/Prototypes/Entities/Structures/Doors/airlock_groups.yml
+++ /dev/null
@@ -1,86 +0,0 @@
-- type: AirlockGroup
- id: Standard
- iconPriority: 100
- stylePaths:
- atmospherics: Structures/Doors/Airlocks/Standard/atmospherics.rsi
- basic: Structures/Doors/Airlocks/Standard/basic.rsi
- cargo: Structures/Doors/Airlocks/Standard/cargo.rsi
- chemistry: Structures/Doors/Airlocks/Standard/chemistry.rsi
- command: Structures/Doors/Airlocks/Standard/command.rsi
- engineering: Structures/Doors/Airlocks/Standard/engineering.rsi
- freezer: Structures/Doors/Airlocks/Standard/freezer.rsi
- hydroponics: Structures/Doors/Airlocks/Standard/hydroponics.rsi
- maintenance: Structures/Doors/Airlocks/Standard/maint.rsi
- medical: Structures/Doors/Airlocks/Standard/medical.rsi
- science: Structures/Doors/Airlocks/Standard/science.rsi
- security: Structures/Doors/Airlocks/Standard/security.rsi
- virology: Structures/Doors/Airlocks/Standard/virology.rsi
- mercenary: _NF/Structures/Doors/Airlocks/Standard/mercenary.rsi
-
-- type: AirlockGroup
- id: Glass
- iconPriority: 90
- stylePaths:
- atmospherics: Structures/Doors/Airlocks/Glass/atmospherics.rsi
- basic: Structures/Doors/Airlocks/Glass/basic.rsi
- cargo: Structures/Doors/Airlocks/Glass/cargo.rsi
- command: Structures/Doors/Airlocks/Glass/command.rsi
- chemistry: Structures/Doors/Airlocks/Glass/chemistry.rsi
- science: Structures/Doors/Airlocks/Glass/science.rsi
- engineering: Structures/Doors/Airlocks/Glass/engineering.rsi
- glass: Structures/Doors/Airlocks/Glass/glass.rsi
- hydroponics: Structures/Doors/Airlocks/Glass/hydroponics.rsi
- maintenance: Structures/Doors/Airlocks/Glass/maint.rsi
- medical: Structures/Doors/Airlocks/Glass/medical.rsi
- security: Structures/Doors/Airlocks/Glass/security.rsi
- virology: Structures/Doors/Airlocks/Glass/virology.rsi
- mercenary: _NF/Structures/Doors/Airlocks/Glass/mercenary.rsi
-
-- type: AirlockGroup
- id: Windoor
- iconPriority: 80
- stylePaths:
- basic: Structures/Doors/Airlocks/Glass/glass.rsi
-
-- type: AirlockGroup
- id: External
- iconPriority: 70
- stylePaths:
- external: Structures/Doors/Airlocks/Standard/external.rsi
-
-- type: AirlockGroup
- id: ExternalGlass
- iconPriority: 60
- stylePaths:
- external: Structures/Doors/Airlocks/Glass/external.rsi
-
-- type: AirlockGroup
- id: Shuttle
- iconPriority: 50
- stylePaths:
- shuttle: Structures/Doors/Airlocks/Standard/shuttle.rsi
-
-- type: AirlockGroup
- id: ShuttleGlass
- iconPriority: 40
- stylePaths:
- shuttle: Structures/Doors/Airlocks/Glass/shuttle.rsi
-
-# 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
diff --git a/Resources/Prototypes/Entities/Structures/Storage/Canisters/gas_canisters.yml b/Resources/Prototypes/Entities/Structures/Storage/Canisters/gas_canisters.yml
index 713d5c1fa75..fb1df2cedde 100644
--- a/Resources/Prototypes/Entities/Structures/Storage/Canisters/gas_canisters.yml
+++ b/Resources/Prototypes/Entities/Structures/Storage/Canisters/gas_canisters.yml
@@ -10,10 +10,10 @@
- type: Transform
noRot: true
- type: Sprite
- sprite: _NF/Structures/Storage/canister.rsi # Frontier
+ sprite: _NF/Structures/Storage/canister.rsi
noRot: true
layers:
- - state: air # Frontier
+ - state: air
- type: Appearance
- type: GenericVisualizer
visuals:
@@ -35,6 +35,9 @@
1: { state: can-o1, shader: "unshaded" }
2: { state: can-o2, shader: "unshaded" }
3: { state: can-o3, shader: "unshaded" }
+ - type: ActivatableUI
+ key: enum.GasCanisterUiKey.Key
+ - type: ActivatableUIRequiresLock
- type: UserInterface
interfaces:
enum.GasCanisterUiKey.Key:
@@ -113,10 +116,8 @@
- type: GuideHelp
guides:
- GasCanisters
- - type: Climbable # mono
- - type: FootstepModifier # mono
- footstepSoundCollection:
- collection: FootstepHull
+ - type: Paintable
+ group: Canisters
- type: entity
parent: GasCanister
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..a30ae00e961 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
+ group: Locker
- 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 b625801c0f8..cb6b1963428 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
+ group: null # not shaped like other lockers
# Basic
- type: entity
@@ -174,6 +176,8 @@
node: done
containers:
- entity_storage
+ - type: Paintable
+ group: null
- 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 bdff9216b4e..297c7f8aead 100644
--- a/Resources/Prototypes/Entities/Structures/Storage/Closets/base_structureclosets.yml
+++ b/Resources/Prototypes/Entities/Structures/Storage/Closets/base_structureclosets.yml
@@ -124,6 +124,8 @@
node: done
containers:
- entity_storage
+ - type: Paintable
+ group: Closet
#Wall Closet
- type: entity
@@ -200,6 +202,8 @@
node: done
containers:
- entity_storage
+ - type: Paintable
+ group: WallCloset
#Wall locker
- type: entity
@@ -223,6 +227,8 @@
- state: welded
visible: false
map: ["enum.WeldableLayers.BaseWelded"]
+ - type: Paintable
+ group: WallLocker
#Base suit storage unit
#I am terribly sorry for duplicating the closet almost-wholesale, but the game malds at me if I don't so here we are.
diff --git a/Resources/Prototypes/Entities/Structures/Storage/Crates/base_structurecrates.yml b/Resources/Prototypes/Entities/Structures/Storage/Crates/base_structurecrates.yml
index abb5e2d605e..125a2d0fadb 100644
--- a/Resources/Prototypes/Entities/Structures/Storage/Crates/base_structurecrates.yml
+++ b/Resources/Prototypes/Entities/Structures/Storage/Crates/base_structurecrates.yml
@@ -154,3 +154,5 @@
- Energy
reflectProb: 0.2
spread: 90
+ - type: Paintable
+ group: CrateSecure
diff --git a/Resources/Prototypes/Entities/Structures/Storage/Crates/crates.yml b/Resources/Prototypes/Entities/Structures/Storage/Crates/crates.yml
index 486e23300c3..95b54720c28 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
+ group: CrateSteel
- type: RadiationBlockingContainer
resistance: 2.5
@@ -31,6 +33,8 @@
- entity_storage
- type: StaticPrice
price: 80 # Frontier: 100<80 - TODO: material value rework
+ - type: Paintable
+ group: CratePlastic
- type: entity
parent: CrateBaseWeldable
@@ -47,7 +51,7 @@
containers:
- entity_storage
- type: StaticPrice
- price: 40 # Monolith
+ price: 40
- type: entity
parent: CratePlastic
@@ -66,6 +70,8 @@
node: done
containers:
- entity_storage
+ - type: Paintable
+ group: null
- type: entity
parent: CratePlastic
@@ -172,6 +178,15 @@
damageModifierSet: Web
- type: Destructible
thresholds:
+ - trigger: # Excess damage, don't spawn entities
+ !type:DamageTrigger
+ damage: 100
+ behaviors:
+ - !type:PlaySoundBehavior
+ sound:
+ collection: WoodDestroy
+ - !type:DoActsBehavior
+ acts: ["Destruction"]
- trigger:
!type:DamageTrigger
damage: 50
@@ -513,6 +528,15 @@
state: base
- type: Destructible
thresholds:
+ - trigger: # Excess damage, don't spawn entities
+ !type:DamageTrigger
+ damage: 75
+ behaviors:
+ - !type:PlaySoundBehavior
+ sound:
+ collection: WoodDestroy
+ - !type:DoActsBehavior
+ acts: ["Destruction"]
- trigger:
!type:DamageTrigger
damage: 15
@@ -555,6 +579,15 @@
state: base
- type: Destructible
thresholds:
+ - trigger: # Excess damage, don't spawn entities
+ !type:DamageTrigger
+ damage: 400
+ behaviors:
+ - !type:PlaySoundBehavior
+ sound:
+ collection: WoodDestroy
+ - !type:DoActsBehavior
+ acts: ["Destruction"]
- trigger:
!type:DamageTrigger
damage: 200 # discourage just beating the grave to break it open
@@ -633,6 +666,8 @@
sprite: Structures/Storage/Crates/labels.rsi
offset: "0.0,0.03125"
map: ["enum.PaperLabelVisuals.Layer"]
+ - type: Paintable
+ group: null
- type: entity
parent: CrateBaseSecure
@@ -659,6 +694,9 @@
map: ["enum.PaperLabelVisuals.Layer"]
- type: AccessReader
# access: [["Janitor"]]
+ access: [["Janitor"]]
+ - type: Paintable
+ group: null
- type: entity
parent: CrateBaseWeldable
diff --git a/Resources/Prototypes/Entities/Structures/Walls/Rock/andesite.yml b/Resources/Prototypes/Entities/Structures/Walls/Rock/andesite.yml
new file mode 100644
index 00000000000..fdb10595d51
--- /dev/null
+++ b/Resources/Prototypes/Entities/Structures/Walls/Rock/andesite.yml
@@ -0,0 +1,188 @@
+# Andesite variants
+- type: entity
+ id: WallRockAndesite
+ name: andesite
+ parent: NFWallRockOre # Frontier: WallRock
# Null-Rogue Combined-Sectors Program
-You are yet another crewman inhabiting the Null Sector! You're one of the lucky survivors hoping with the development of the furthest reaches of space, fighting back to survive any Armada interventions that may happen along the way!
+ You are an inhabitant of the Null Sector, emerging from your asteroid dwelling or from a local resting safe-spot.
+ You're one of the luckier ones, as most have been taken by the Armada. Your goal is to explore these reaches of space, gather the wherewithal to survive, and to fight any invaders for your right to live. Not everything is explained: adventure is all about finding what's out there, for better or worse.
-[color=#33bbff]If you'd like a hand, ask on the radio, or talk to somebody.[/color]
-The [color=#dd0000]Valet[/color] or [color=#009933]Sector Coordinator[/color] should be able to help, they're to the lower-right of medbay.
+ [color=#009933]If you'd like a hand, ask on the radio, or talk to somebody.[/color]
+If a [color=#dd0000]Valet[/color] or [color=#33bbff]Sector Coordinator[/color] (SC) is available, they may help. The SC's office is to the southeast of medbay.
+
+ Nearby the SC's office are the Automated Trade Machines (ATMs), and Shipyard terminals.
## Economy
@@ -12,21 +15,34 @@ The [color=#dd0000]Valet[/color] or [color=#009933]Sector Coordinator[/color] sh
- Any money that you make is [bold]yours[/bold], and will be available in future shifts if deposited at a [color=#44cc00]Bank ATM[/color] on Lark Station, the Expeditionary Lodge, or Tinnia's Rest.
+ Any money that you make is [bold]yours[/bold], and will be available in future shifts if deposited at a [color=#44cc00]Bank ATM[/color] on Lark Station, the Expeditionary Lodge, or any other Point of Interest (PoI) that has an ATM.
+
+ To make money, why not:
+ - Join a ship's crew? Ask on the radio if anyone needs extra crew, and work to help your captain.
+ - Get a job on station? Ask the [color=#009933]Sector Coordinator[/color] (follow the SC signs) if anything needs doing.
+ - Use a [color=#cccc00]shipyard console[/color] to purchase your own ship?
+
+ Money allows one to spend it on various amenities, weapons, new ships and more. For most, it is their primary objective. For others, it is just another resource to consider.
+
+ ## Amenities
+
+
+
+
+ Some provisions (e.g. air tanks, salvage equipment) are available from vendors on Lark Station, but most amenities (medical services, food) should be offered by players on shuttles or at the Trade Depot.
+
+ Be sure to stock up on spare fuel before any extended voyages, and take advantage of the [color=#33bbff]medical implanter[/color] in your loadout, or from a NanoMed vendor.
+
+ Shifts in the Null Sector typically last for [color=#33bbff]Seven Hours[/color]. Though you are not obligated to stay. If you need to end your ventures early or take a break, use the [color=#33bbff]cryo sleep chambers[/color] in the medbay on Lark or Kemak Stations.
-To make money, why not:
-- Join a ship's crew? Ask on the radio if anyone needs extra crew, and work to help your captain.
-- Get a job on station? Ask the [color=#009933]Sector Coordinator[/color] (follow the SC signs) if anything needs doing.
-- Use a [color=#cccc00]shipyard console[/color] to purchase your own ship?
+ When a shift ends, it is typically due to an oncoming automated Armadan scout vessel on its routine patrol. Anyone who wants to make sure they live to tell the tale should sell their vessel as soon as the warning is sounded from the Lark Relay.
-## Amenities
+ ### Food
-
-
+
+
- Some provisions (e.g. air tanks, salvage equipment) are available from vendors on Lark Station, but most amenities (medical services, food) should be offered by players on shuttles or at the Trade Depot.
-Be sure to stock up at the Trade Depot if going out on an extended voyage, and take advantage of the [color=#33bbff]medical insurance tracker[/color] in your loadout, or from the NanoMed vendor in medbay.
+ Expedition-goers will have it easy, as food and food-prep materials are quick to find out there. Everyone else has to scavenge for food, create it using biomass at kemak, or eat crap organs and die. It is scarce for those who don't know where to look, so treasure what you have.
-Shifts in the Null Sector typically last for [color=#33bbff]Seven Hours[/color]. Though you are not obligated to stay. If you need to end your ventures early or take a break, use the [color=#33bbff]cryo sleep chambers[/color] in the medbay on Lark Station.
diff --git a/Resources/Textures/Objects/Tools/spray_painter.rsi/ammo-inhand-left.png b/Resources/Textures/Objects/Tools/spray_painter.rsi/ammo-inhand-left.png
new file mode 100644
index 00000000000..d81e805bb25
Binary files /dev/null and b/Resources/Textures/Objects/Tools/spray_painter.rsi/ammo-inhand-left.png differ
diff --git a/Resources/Textures/Objects/Tools/spray_painter.rsi/ammo-inhand-right.png b/Resources/Textures/Objects/Tools/spray_painter.rsi/ammo-inhand-right.png
new file mode 100644
index 00000000000..b4083c87e1e
Binary files /dev/null and b/Resources/Textures/Objects/Tools/spray_painter.rsi/ammo-inhand-right.png differ
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 00000000000..ca64c2a7580
Binary files /dev/null and b/Resources/Textures/Objects/Tools/spray_painter.rsi/ammo.png differ
diff --git a/Resources/Textures/Objects/Tools/spray_painter.rsi/inhand-left.png b/Resources/Textures/Objects/Tools/spray_painter.rsi/inhand-left.png
new file mode 100644
index 00000000000..34787a11f5f
Binary files /dev/null and b/Resources/Textures/Objects/Tools/spray_painter.rsi/inhand-left.png differ
diff --git a/Resources/Textures/Objects/Tools/spray_painter.rsi/inhand-right.png b/Resources/Textures/Objects/Tools/spray_painter.rsi/inhand-right.png
new file mode 100644
index 00000000000..ce4df846069
Binary files /dev/null and b/Resources/Textures/Objects/Tools/spray_painter.rsi/inhand-right.png differ
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
+ }
+ ]
}
diff --git a/Resources/Textures/Structures/Storage/Crates/wood.rsi/closed.png b/Resources/Textures/Structures/Storage/Crates/wood.rsi/closed.png
index 472214c5209..58895d592e8 100644
Binary files a/Resources/Textures/Structures/Storage/Crates/wood.rsi/closed.png and b/Resources/Textures/Structures/Storage/Crates/wood.rsi/closed.png differ
diff --git a/Resources/Textures/Structures/Storage/Crates/wood.rsi/icon.png b/Resources/Textures/Structures/Storage/Crates/wood.rsi/icon.png
index e93350a1b2b..fb3fe5b27e5 100644
Binary files a/Resources/Textures/Structures/Storage/Crates/wood.rsi/icon.png and b/Resources/Textures/Structures/Storage/Crates/wood.rsi/icon.png differ
diff --git a/Resources/Textures/Structures/Walls/rock.rsi/meta.json b/Resources/Textures/Structures/Walls/rock.rsi/meta.json
index a29513356c5..9ec12987a68 100644
--- a/Resources/Textures/Structures/Walls/rock.rsi/meta.json
+++ b/Resources/Textures/Structures/Walls/rock.rsi/meta.json
@@ -155,6 +155,12 @@
{
"name": "rock_tin"
},
+ {
+ "name": "rock_scrap"
+ },
+ {
+ "name": "rock_bluespace"
+ },
{
"name": "rock_uranium"
},
diff --git a/Resources/Textures/Structures/Walls/rock.rsi/rock_bluespace.png b/Resources/Textures/Structures/Walls/rock.rsi/rock_bluespace.png
new file mode 100644
index 00000000000..e3368c34d71
Binary files /dev/null and b/Resources/Textures/Structures/Walls/rock.rsi/rock_bluespace.png differ
diff --git a/Resources/Textures/Structures/Walls/rock.rsi/rock_scrap.png b/Resources/Textures/Structures/Walls/rock.rsi/rock_scrap.png
new file mode 100644
index 00000000000..56274c584af
Binary files /dev/null and b/Resources/Textures/Structures/Walls/rock.rsi/rock_scrap.png differ
diff --git a/Resources/Textures/Tiles/lattice.png b/Resources/Textures/Tiles/lattice.png
index 9a33ae0f9a0..53657f0538e 100644
Binary files a/Resources/Textures/Tiles/lattice.png and b/Resources/Textures/Tiles/lattice.png differ
diff --git a/Resources/Textures/_NF/Decals/stencil.rsi/meta.json b/Resources/Textures/_NF/Decals/stencil.rsi/meta.json
new file mode 100644
index 00000000000..69cebf29d0f
--- /dev/null
+++ b/Resources/Textures/_NF/Decals/stencil.rsi/meta.json
@@ -0,0 +1,149 @@
+{
+ "version": 1,
+ "license": "CC-BY-SA-4.0",
+ "copyright": "made by Alkheemist (GitHub/Discord)",
+ "size": {
+ "x": 32,
+ "y": 32
+ },
+ "states": [
+ {
+ "name": "stencil0"
+ },
+ {
+ "name": "stencil1"
+ },
+ {
+ "name": "stencil2"
+ },
+ {
+ "name": "stencil3"
+ },
+ {
+ "name": "stencil4"
+ },
+ {
+ "name": "stencil5"
+ },
+ {
+ "name": "stencil6"
+ },
+ {
+ "name": "stencil7"
+ },
+ {
+ "name": "stencil8"
+ },
+ {
+ "name": "stencil9"
+ },
+ {
+ "name": "stencilA"
+ },
+ {
+ "name": "stencilB"
+ },
+ {
+ "name": "stencilC"
+ },
+ {
+ "name": "stencilD"
+ },
+ {
+ "name": "stencilE"
+ },
+ {
+ "name": "stencilF"
+ },
+ {
+ "name": "stencilG"
+ },
+ {
+ "name": "stencilH"
+ },
+ {
+ "name": "stencilI"
+ },
+ {
+ "name": "stencilJ"
+ },
+ {
+ "name": "stencilK"
+ },
+ {
+ "name": "stencilL"
+ },
+ {
+ "name": "stencilM"
+ },
+ {
+ "name": "stencilN"
+ },
+ {
+ "name": "stencilO"
+ },
+ {
+ "name": "stencilP"
+ },
+ {
+ "name": "stencilQ"
+ },
+ {
+ "name": "stencilR"
+ },
+ {
+ "name": "stencilS"
+ },
+ {
+ "name": "stencilT"
+ },
+ {
+ "name": "stencilU"
+ },
+ {
+ "name": "stencilV"
+ },
+ {
+ "name": "stencilW"
+ },
+ {
+ "name": "stencilX"
+ },
+ {
+ "name": "stencilY"
+ },
+ {
+ "name": "stencilZ"
+ },
+ {
+ "name": "stencil_Ampersand"
+ },
+ {
+ "name": "stencil_Asterix"
+ },
+ {
+ "name": "stencil_Dash"
+ },
+ {
+ "name": "stencil_Equals"
+ },
+ {
+ "name": "stencil_Exclaim"
+ },
+ {
+ "name": "stencil_Hash"
+ },
+ {
+ "name": "stencil_Speso"
+ },
+ {
+ "name": "stencil_Multiocular"
+ },
+ {
+ "name": "stencil_Plus"
+ },
+ {
+ "name": "stencil_Question"
+ }
+ ]
+}
diff --git a/Resources/Textures/_NF/Decals/stencil.rsi/stencil0.png b/Resources/Textures/_NF/Decals/stencil.rsi/stencil0.png
new file mode 100644
index 00000000000..5c313927469
Binary files /dev/null and b/Resources/Textures/_NF/Decals/stencil.rsi/stencil0.png differ
diff --git a/Resources/Textures/_NF/Decals/stencil.rsi/stencil1.png b/Resources/Textures/_NF/Decals/stencil.rsi/stencil1.png
new file mode 100644
index 00000000000..aaf94006b44
Binary files /dev/null and b/Resources/Textures/_NF/Decals/stencil.rsi/stencil1.png differ
diff --git a/Resources/Textures/_NF/Decals/stencil.rsi/stencil2.png b/Resources/Textures/_NF/Decals/stencil.rsi/stencil2.png
new file mode 100644
index 00000000000..4c2b20879b4
Binary files /dev/null and b/Resources/Textures/_NF/Decals/stencil.rsi/stencil2.png differ
diff --git a/Resources/Textures/_NF/Decals/stencil.rsi/stencil3.png b/Resources/Textures/_NF/Decals/stencil.rsi/stencil3.png
new file mode 100644
index 00000000000..2e32055f76d
Binary files /dev/null and b/Resources/Textures/_NF/Decals/stencil.rsi/stencil3.png differ
diff --git a/Resources/Textures/_NF/Decals/stencil.rsi/stencil4.png b/Resources/Textures/_NF/Decals/stencil.rsi/stencil4.png
new file mode 100644
index 00000000000..041dbb45c20
Binary files /dev/null and b/Resources/Textures/_NF/Decals/stencil.rsi/stencil4.png differ
diff --git a/Resources/Textures/_NF/Decals/stencil.rsi/stencil5.png b/Resources/Textures/_NF/Decals/stencil.rsi/stencil5.png
new file mode 100644
index 00000000000..0fac5c690bf
Binary files /dev/null and b/Resources/Textures/_NF/Decals/stencil.rsi/stencil5.png differ
diff --git a/Resources/Textures/_NF/Decals/stencil.rsi/stencil6.png b/Resources/Textures/_NF/Decals/stencil.rsi/stencil6.png
new file mode 100644
index 00000000000..46ceaf0d565
Binary files /dev/null and b/Resources/Textures/_NF/Decals/stencil.rsi/stencil6.png differ
diff --git a/Resources/Textures/_NF/Decals/stencil.rsi/stencil7.png b/Resources/Textures/_NF/Decals/stencil.rsi/stencil7.png
new file mode 100644
index 00000000000..54826779ced
Binary files /dev/null and b/Resources/Textures/_NF/Decals/stencil.rsi/stencil7.png differ
diff --git a/Resources/Textures/_NF/Decals/stencil.rsi/stencil8.png b/Resources/Textures/_NF/Decals/stencil.rsi/stencil8.png
new file mode 100644
index 00000000000..7c97b60c89b
Binary files /dev/null and b/Resources/Textures/_NF/Decals/stencil.rsi/stencil8.png differ
diff --git a/Resources/Textures/_NF/Decals/stencil.rsi/stencil9.png b/Resources/Textures/_NF/Decals/stencil.rsi/stencil9.png
new file mode 100644
index 00000000000..d0c4a680d2a
Binary files /dev/null and b/Resources/Textures/_NF/Decals/stencil.rsi/stencil9.png differ
diff --git a/Resources/Textures/_NF/Decals/stencil.rsi/stencilA.png b/Resources/Textures/_NF/Decals/stencil.rsi/stencilA.png
new file mode 100644
index 00000000000..c3a92548260
Binary files /dev/null and b/Resources/Textures/_NF/Decals/stencil.rsi/stencilA.png differ
diff --git a/Resources/Textures/_NF/Decals/stencil.rsi/stencilB.png b/Resources/Textures/_NF/Decals/stencil.rsi/stencilB.png
new file mode 100644
index 00000000000..fc62e40c950
Binary files /dev/null and b/Resources/Textures/_NF/Decals/stencil.rsi/stencilB.png differ
diff --git a/Resources/Textures/_NF/Decals/stencil.rsi/stencilC.png b/Resources/Textures/_NF/Decals/stencil.rsi/stencilC.png
new file mode 100644
index 00000000000..e79cf206cc8
Binary files /dev/null and b/Resources/Textures/_NF/Decals/stencil.rsi/stencilC.png differ
diff --git a/Resources/Textures/_NF/Decals/stencil.rsi/stencilD.png b/Resources/Textures/_NF/Decals/stencil.rsi/stencilD.png
new file mode 100644
index 00000000000..649f4043a55
Binary files /dev/null and b/Resources/Textures/_NF/Decals/stencil.rsi/stencilD.png differ
diff --git a/Resources/Textures/_NF/Decals/stencil.rsi/stencilE.png b/Resources/Textures/_NF/Decals/stencil.rsi/stencilE.png
new file mode 100644
index 00000000000..a387756bbc9
Binary files /dev/null and b/Resources/Textures/_NF/Decals/stencil.rsi/stencilE.png differ
diff --git a/Resources/Textures/_NF/Decals/stencil.rsi/stencilF.png b/Resources/Textures/_NF/Decals/stencil.rsi/stencilF.png
new file mode 100644
index 00000000000..e50436bd474
Binary files /dev/null and b/Resources/Textures/_NF/Decals/stencil.rsi/stencilF.png differ
diff --git a/Resources/Textures/_NF/Decals/stencil.rsi/stencilG.png b/Resources/Textures/_NF/Decals/stencil.rsi/stencilG.png
new file mode 100644
index 00000000000..3e1fd2acabd
Binary files /dev/null and b/Resources/Textures/_NF/Decals/stencil.rsi/stencilG.png differ
diff --git a/Resources/Textures/_NF/Decals/stencil.rsi/stencilH.png b/Resources/Textures/_NF/Decals/stencil.rsi/stencilH.png
new file mode 100644
index 00000000000..26e8f89348c
Binary files /dev/null and b/Resources/Textures/_NF/Decals/stencil.rsi/stencilH.png differ
diff --git a/Resources/Textures/_NF/Decals/stencil.rsi/stencilI.png b/Resources/Textures/_NF/Decals/stencil.rsi/stencilI.png
new file mode 100644
index 00000000000..3c8bc08bc73
Binary files /dev/null and b/Resources/Textures/_NF/Decals/stencil.rsi/stencilI.png differ
diff --git a/Resources/Textures/_NF/Decals/stencil.rsi/stencilJ.png b/Resources/Textures/_NF/Decals/stencil.rsi/stencilJ.png
new file mode 100644
index 00000000000..360ffb4fab2
Binary files /dev/null and b/Resources/Textures/_NF/Decals/stencil.rsi/stencilJ.png differ
diff --git a/Resources/Textures/_NF/Decals/stencil.rsi/stencilK.png b/Resources/Textures/_NF/Decals/stencil.rsi/stencilK.png
new file mode 100644
index 00000000000..7c5dcd10a3c
Binary files /dev/null and b/Resources/Textures/_NF/Decals/stencil.rsi/stencilK.png differ
diff --git a/Resources/Textures/_NF/Decals/stencil.rsi/stencilL.png b/Resources/Textures/_NF/Decals/stencil.rsi/stencilL.png
new file mode 100644
index 00000000000..cc578c18b5c
Binary files /dev/null and b/Resources/Textures/_NF/Decals/stencil.rsi/stencilL.png differ
diff --git a/Resources/Textures/_NF/Decals/stencil.rsi/stencilM.png b/Resources/Textures/_NF/Decals/stencil.rsi/stencilM.png
new file mode 100644
index 00000000000..f69505f58be
Binary files /dev/null and b/Resources/Textures/_NF/Decals/stencil.rsi/stencilM.png differ
diff --git a/Resources/Textures/_NF/Decals/stencil.rsi/stencilN.png b/Resources/Textures/_NF/Decals/stencil.rsi/stencilN.png
new file mode 100644
index 00000000000..0052eeba7cd
Binary files /dev/null and b/Resources/Textures/_NF/Decals/stencil.rsi/stencilN.png differ
diff --git a/Resources/Textures/_NF/Decals/stencil.rsi/stencilO.png b/Resources/Textures/_NF/Decals/stencil.rsi/stencilO.png
new file mode 100644
index 00000000000..d8b11a7d9c3
Binary files /dev/null and b/Resources/Textures/_NF/Decals/stencil.rsi/stencilO.png differ
diff --git a/Resources/Textures/_NF/Decals/stencil.rsi/stencilP.png b/Resources/Textures/_NF/Decals/stencil.rsi/stencilP.png
new file mode 100644
index 00000000000..dd7ef904272
Binary files /dev/null and b/Resources/Textures/_NF/Decals/stencil.rsi/stencilP.png differ
diff --git a/Resources/Textures/_NF/Decals/stencil.rsi/stencilQ.png b/Resources/Textures/_NF/Decals/stencil.rsi/stencilQ.png
new file mode 100644
index 00000000000..b4af8484176
Binary files /dev/null and b/Resources/Textures/_NF/Decals/stencil.rsi/stencilQ.png differ
diff --git a/Resources/Textures/_NF/Decals/stencil.rsi/stencilR.png b/Resources/Textures/_NF/Decals/stencil.rsi/stencilR.png
new file mode 100644
index 00000000000..bb4f0a2d040
Binary files /dev/null and b/Resources/Textures/_NF/Decals/stencil.rsi/stencilR.png differ
diff --git a/Resources/Textures/_NF/Decals/stencil.rsi/stencilS.png b/Resources/Textures/_NF/Decals/stencil.rsi/stencilS.png
new file mode 100644
index 00000000000..4a1a69e1b02
Binary files /dev/null and b/Resources/Textures/_NF/Decals/stencil.rsi/stencilS.png differ
diff --git a/Resources/Textures/_NF/Decals/stencil.rsi/stencilT.png b/Resources/Textures/_NF/Decals/stencil.rsi/stencilT.png
new file mode 100644
index 00000000000..f895a92edda
Binary files /dev/null and b/Resources/Textures/_NF/Decals/stencil.rsi/stencilT.png differ
diff --git a/Resources/Textures/_NF/Decals/stencil.rsi/stencilU.png b/Resources/Textures/_NF/Decals/stencil.rsi/stencilU.png
new file mode 100644
index 00000000000..2e09e218b96
Binary files /dev/null and b/Resources/Textures/_NF/Decals/stencil.rsi/stencilU.png differ
diff --git a/Resources/Textures/_NF/Decals/stencil.rsi/stencilV.png b/Resources/Textures/_NF/Decals/stencil.rsi/stencilV.png
new file mode 100644
index 00000000000..572e40bf95b
Binary files /dev/null and b/Resources/Textures/_NF/Decals/stencil.rsi/stencilV.png differ
diff --git a/Resources/Textures/_NF/Decals/stencil.rsi/stencilW.png b/Resources/Textures/_NF/Decals/stencil.rsi/stencilW.png
new file mode 100644
index 00000000000..29cb98300e1
Binary files /dev/null and b/Resources/Textures/_NF/Decals/stencil.rsi/stencilW.png differ
diff --git a/Resources/Textures/_NF/Decals/stencil.rsi/stencilX.png b/Resources/Textures/_NF/Decals/stencil.rsi/stencilX.png
new file mode 100644
index 00000000000..d76ab6de061
Binary files /dev/null and b/Resources/Textures/_NF/Decals/stencil.rsi/stencilX.png differ
diff --git a/Resources/Textures/_NF/Decals/stencil.rsi/stencilY.png b/Resources/Textures/_NF/Decals/stencil.rsi/stencilY.png
new file mode 100644
index 00000000000..9f5f0ed52f8
Binary files /dev/null and b/Resources/Textures/_NF/Decals/stencil.rsi/stencilY.png differ
diff --git a/Resources/Textures/_NF/Decals/stencil.rsi/stencilZ.png b/Resources/Textures/_NF/Decals/stencil.rsi/stencilZ.png
new file mode 100644
index 00000000000..7c40d838a30
Binary files /dev/null and b/Resources/Textures/_NF/Decals/stencil.rsi/stencilZ.png differ
diff --git a/Resources/Textures/_NF/Decals/stencil.rsi/stencil_Ampersand.png b/Resources/Textures/_NF/Decals/stencil.rsi/stencil_Ampersand.png
new file mode 100644
index 00000000000..ac7583bf5f8
Binary files /dev/null and b/Resources/Textures/_NF/Decals/stencil.rsi/stencil_Ampersand.png differ
diff --git a/Resources/Textures/_NF/Decals/stencil.rsi/stencil_Asterix.png b/Resources/Textures/_NF/Decals/stencil.rsi/stencil_Asterix.png
new file mode 100644
index 00000000000..37ca93425fa
Binary files /dev/null and b/Resources/Textures/_NF/Decals/stencil.rsi/stencil_Asterix.png differ
diff --git a/Resources/Textures/_NF/Decals/stencil.rsi/stencil_Dash.png b/Resources/Textures/_NF/Decals/stencil.rsi/stencil_Dash.png
new file mode 100644
index 00000000000..c77f630a77c
Binary files /dev/null and b/Resources/Textures/_NF/Decals/stencil.rsi/stencil_Dash.png differ
diff --git a/Resources/Textures/_NF/Decals/stencil.rsi/stencil_Equals.png b/Resources/Textures/_NF/Decals/stencil.rsi/stencil_Equals.png
new file mode 100644
index 00000000000..caefdf09947
Binary files /dev/null and b/Resources/Textures/_NF/Decals/stencil.rsi/stencil_Equals.png differ
diff --git a/Resources/Textures/_NF/Decals/stencil.rsi/stencil_Exclaim.png b/Resources/Textures/_NF/Decals/stencil.rsi/stencil_Exclaim.png
new file mode 100644
index 00000000000..9b7024bc539
Binary files /dev/null and b/Resources/Textures/_NF/Decals/stencil.rsi/stencil_Exclaim.png differ
diff --git a/Resources/Textures/_NF/Decals/stencil.rsi/stencil_Hash.png b/Resources/Textures/_NF/Decals/stencil.rsi/stencil_Hash.png
new file mode 100644
index 00000000000..1e22e4b779e
Binary files /dev/null and b/Resources/Textures/_NF/Decals/stencil.rsi/stencil_Hash.png differ
diff --git a/Resources/Textures/_NF/Decals/stencil.rsi/stencil_Multiocular.png b/Resources/Textures/_NF/Decals/stencil.rsi/stencil_Multiocular.png
new file mode 100644
index 00000000000..284401a1880
Binary files /dev/null and b/Resources/Textures/_NF/Decals/stencil.rsi/stencil_Multiocular.png differ
diff --git a/Resources/Textures/_NF/Decals/stencil.rsi/stencil_Plus.png b/Resources/Textures/_NF/Decals/stencil.rsi/stencil_Plus.png
new file mode 100644
index 00000000000..e50358d2840
Binary files /dev/null and b/Resources/Textures/_NF/Decals/stencil.rsi/stencil_Plus.png differ
diff --git a/Resources/Textures/_NF/Decals/stencil.rsi/stencil_Question.png b/Resources/Textures/_NF/Decals/stencil.rsi/stencil_Question.png
new file mode 100644
index 00000000000..63fd0ad8f2f
Binary files /dev/null and b/Resources/Textures/_NF/Decals/stencil.rsi/stencil_Question.png differ
diff --git a/Resources/Textures/_NF/Decals/stencil.rsi/stencil_Speso.png b/Resources/Textures/_NF/Decals/stencil.rsi/stencil_Speso.png
new file mode 100644
index 00000000000..a6c97f78596
Binary files /dev/null and b/Resources/Textures/_NF/Decals/stencil.rsi/stencil_Speso.png differ
diff --git a/Resources/Textures/_NF/Objects/Fun/magic_spraypainter.rsi/icon-inhand-left.png b/Resources/Textures/_NF/Objects/Fun/magic_spraypainter.rsi/icon-inhand-left.png
new file mode 100644
index 00000000000..d3f64053db2
Binary files /dev/null and b/Resources/Textures/_NF/Objects/Fun/magic_spraypainter.rsi/icon-inhand-left.png differ
diff --git a/Resources/Textures/_NF/Objects/Fun/magic_spraypainter.rsi/icon-inhand-right.png b/Resources/Textures/_NF/Objects/Fun/magic_spraypainter.rsi/icon-inhand-right.png
new file mode 100644
index 00000000000..c5ce26d90dd
Binary files /dev/null and b/Resources/Textures/_NF/Objects/Fun/magic_spraypainter.rsi/icon-inhand-right.png differ
diff --git a/Resources/Textures/_NF/Objects/Fun/magic_spraypainter.rsi/icon.png b/Resources/Textures/_NF/Objects/Fun/magic_spraypainter.rsi/icon.png
new file mode 100644
index 00000000000..9c4ebda950b
Binary files /dev/null and b/Resources/Textures/_NF/Objects/Fun/magic_spraypainter.rsi/icon.png differ
diff --git a/Resources/Textures/_NF/Objects/Fun/magic_spraypainter.rsi/meta.json b/Resources/Textures/_NF/Objects/Fun/magic_spraypainter.rsi/meta.json
new file mode 100644
index 00000000000..79c0a4915a9
--- /dev/null
+++ b/Resources/Textures/_NF/Objects/Fun/magic_spraypainter.rsi/meta.json
@@ -0,0 +1,45 @@
+{
+ "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), edited and animated by Worldwaker (github).",
+ "size": {
+ "x": 32,
+ "y": 32
+ },
+ "states": [
+ {
+ "name": "icon",
+ "delays": [
+ [
+ 0.2,
+ 0.2,
+ 0.2,
+ 0.2,
+ 0.2,
+ 0.2,
+ 0.2
+ ]
+ ]
+ },
+ {
+ "name": "icon-inhand-left",
+ "directions": 4,
+ "delays": [
+ [ 0.5, 0.5, 0.5 ],
+ [ 0.5, 0.5, 0.5 ],
+ [ 0.5, 0.5, 0.5 ],
+ [ 0.5, 0.5, 0.5 ]
+ ]
+ },
+ {
+ "name": "icon-inhand-right",
+ "directions": 4,
+ "delays": [
+ [ 0.5, 0.5, 0.5 ],
+ [ 0.5, 0.5, 0.5 ],
+ [ 0.5, 0.5, 0.5 ],
+ [ 0.5, 0.5, 0.5 ]
+ ]
+ }
+ ]
+}
diff --git a/Resources/Textures/_Null/Objects/Fun/eeble.rsi/eeble.png b/Resources/Textures/_Null/Objects/Fun/eeble.rsi/eeble.png
new file mode 100644
index 00000000000..974d6a83da1
Binary files /dev/null and b/Resources/Textures/_Null/Objects/Fun/eeble.rsi/eeble.png differ
diff --git a/Resources/Textures/_Null/Objects/Fun/eeble.rsi/meta.json b/Resources/Textures/_Null/Objects/Fun/eeble.rsi/meta.json
new file mode 100644
index 00000000000..0e70b283ed0
--- /dev/null
+++ b/Resources/Textures/_Null/Objects/Fun/eeble.rsi/meta.json
@@ -0,0 +1,14 @@
+{
+ "version": 1,
+ "license": "CC-BY-SA-3.0",
+ "copyright": "LukeZurg22 for Vault Station & Null Sector",
+ "size": {
+ "x": 8,
+ "y": 8
+ },
+ "states": [
+ {
+ "name": "eeble"
+ }
+ ]
+}
diff --git a/Resources/Textures/_Null/Structures/Walls/scrap.rsi/full.png b/Resources/Textures/_Null/Structures/Walls/scrap.rsi/full.png
new file mode 100644
index 00000000000..1432c676f71
Binary files /dev/null and b/Resources/Textures/_Null/Structures/Walls/scrap.rsi/full.png differ
diff --git a/Resources/Textures/_Null/Structures/Walls/scrap.rsi/meta.json b/Resources/Textures/_Null/Structures/Walls/scrap.rsi/meta.json
new file mode 100644
index 00000000000..1fd4e38556a
--- /dev/null
+++ b/Resources/Textures/_Null/Structures/Walls/scrap.rsi/meta.json
@@ -0,0 +1 @@
+{"version": 1, "size": {"x": 32, "y": 32}, "license": "CC-BY-SA-3.0", "copyright": "https://github.com/vgstation-coders/vgstation13/raw/99cc2ab62d65a3a7b554dc7b21ff5f57c835f973/icons/turf/walls.dmi temporary placeholder for new scrap walls", "states": [{"name": "full"}, {"name": "scrap0", "directions": 4}, {"name": "scrap1", "directions": 4}, {"name": "scrap2", "directions": 4}, {"name": "scrap3", "directions": 4}, {"name": "scrap4", "directions": 4}, {"name": "scrap5", "directions": 4}, {"name": "scrap6", "directions": 4}, {"name": "scrap7", "directions": 4}]}
diff --git a/Resources/Textures/_Null/Structures/Walls/scrap.rsi/scrap0.png b/Resources/Textures/_Null/Structures/Walls/scrap.rsi/scrap0.png
new file mode 100644
index 00000000000..c17ebc0c9e2
Binary files /dev/null and b/Resources/Textures/_Null/Structures/Walls/scrap.rsi/scrap0.png differ
diff --git a/Resources/Textures/_Null/Structures/Walls/scrap.rsi/scrap1.png b/Resources/Textures/_Null/Structures/Walls/scrap.rsi/scrap1.png
new file mode 100644
index 00000000000..8a7f7e774fb
Binary files /dev/null and b/Resources/Textures/_Null/Structures/Walls/scrap.rsi/scrap1.png differ
diff --git a/Resources/Textures/_Null/Structures/Walls/scrap.rsi/scrap2.png b/Resources/Textures/_Null/Structures/Walls/scrap.rsi/scrap2.png
new file mode 100644
index 00000000000..c17ebc0c9e2
Binary files /dev/null and b/Resources/Textures/_Null/Structures/Walls/scrap.rsi/scrap2.png differ
diff --git a/Resources/Textures/_Null/Structures/Walls/scrap.rsi/scrap3.png b/Resources/Textures/_Null/Structures/Walls/scrap.rsi/scrap3.png
new file mode 100644
index 00000000000..8a7f7e774fb
Binary files /dev/null and b/Resources/Textures/_Null/Structures/Walls/scrap.rsi/scrap3.png differ
diff --git a/Resources/Textures/_Null/Structures/Walls/scrap.rsi/scrap4.png b/Resources/Textures/_Null/Structures/Walls/scrap.rsi/scrap4.png
new file mode 100644
index 00000000000..ff6c5e064a1
Binary files /dev/null and b/Resources/Textures/_Null/Structures/Walls/scrap.rsi/scrap4.png differ
diff --git a/Resources/Textures/_Null/Structures/Walls/scrap.rsi/scrap5.png b/Resources/Textures/_Null/Structures/Walls/scrap.rsi/scrap5.png
new file mode 100644
index 00000000000..f77fd38c4f3
Binary files /dev/null and b/Resources/Textures/_Null/Structures/Walls/scrap.rsi/scrap5.png differ
diff --git a/Resources/Textures/_Null/Structures/Walls/scrap.rsi/scrap6.png b/Resources/Textures/_Null/Structures/Walls/scrap.rsi/scrap6.png
new file mode 100644
index 00000000000..ff6c5e064a1
Binary files /dev/null and b/Resources/Textures/_Null/Structures/Walls/scrap.rsi/scrap6.png differ
diff --git a/Resources/Textures/_Null/Structures/Walls/scrap.rsi/scrap7.png b/Resources/Textures/_Null/Structures/Walls/scrap.rsi/scrap7.png
new file mode 100644
index 00000000000..a213f292bed
Binary files /dev/null and b/Resources/Textures/_Null/Structures/Walls/scrap.rsi/scrap7.png differ
diff --git a/Resources/Textures/_Null/Structures/pressure_plate.rsi/icon.png b/Resources/Textures/_Null/Structures/pressure_plate.rsi/icon.png
index c6c622a7c76..2fe3b9de860 100644
Binary files a/Resources/Textures/_Null/Structures/pressure_plate.rsi/icon.png and b/Resources/Textures/_Null/Structures/pressure_plate.rsi/icon.png differ
diff --git a/Resources/Textures/_Null/Structures/pressure_plate.rsi/meta.json b/Resources/Textures/_Null/Structures/pressure_plate.rsi/meta.json
index 667f11b5c3f..78fa280adf4 100644
--- a/Resources/Textures/_Null/Structures/pressure_plate.rsi/meta.json
+++ b/Resources/Textures/_Null/Structures/pressure_plate.rsi/meta.json
@@ -1,7 +1,7 @@
{
"version": 1,
"license": "CC0-1.0",
- "copyright": "By LukeZurg22",
+ "copyright": "By Sketch for Null Sector & TC14",
"size": {
"x": 32,
"y": 32
diff --git a/Resources/Textures/_Null/Structures/pressure_plate.rsi/pressed.png b/Resources/Textures/_Null/Structures/pressure_plate.rsi/pressed.png
index b386a7ad6ff..a1c0e29be99 100644
Binary files a/Resources/Textures/_Null/Structures/pressure_plate.rsi/pressed.png and b/Resources/Textures/_Null/Structures/pressure_plate.rsi/pressed.png differ
diff --git a/Resources/Textures/_Null/Structures/warper.rsi/meta.json b/Resources/Textures/_Null/Structures/warper.rsi/meta.json
new file mode 100644
index 00000000000..66fefecfc0f
--- /dev/null
+++ b/Resources/Textures/_Null/Structures/warper.rsi/meta.json
@@ -0,0 +1,32 @@
+{
+ "version": 1,
+ "license": "CC-BY-SA-3.0",
+ "copyright": "Copyright 2023 Jonas Ljunsgtröm",
+ "size": {
+ "x": 32,
+ "y": 64
+ },
+ "states": [
+ {
+ "name": "warningtape_down",
+ "delays": [
+ [
+ 0.2,
+ 0.2,
+ 0.2,
+ 0.2,
+ 0.2
+ ]
+ ]
+ },
+ {
+ "name": "stair_down"
+ },
+ {
+ "name": "stair_up"
+ },
+ {
+ "name": "warningtape_up"
+ }
+ ]
+}
diff --git a/Resources/Textures/_Null/Structures/warper.rsi/stair_down.png b/Resources/Textures/_Null/Structures/warper.rsi/stair_down.png
new file mode 100644
index 00000000000..d85b34ae25a
Binary files /dev/null and b/Resources/Textures/_Null/Structures/warper.rsi/stair_down.png differ
diff --git a/Resources/Textures/_Null/Structures/warper.rsi/stair_up.png b/Resources/Textures/_Null/Structures/warper.rsi/stair_up.png
new file mode 100644
index 00000000000..1507e0f0554
Binary files /dev/null and b/Resources/Textures/_Null/Structures/warper.rsi/stair_up.png differ
diff --git a/Resources/Textures/_Null/Structures/warper.rsi/warningtape_down.png b/Resources/Textures/_Null/Structures/warper.rsi/warningtape_down.png
new file mode 100644
index 00000000000..4cdbd7229e1
Binary files /dev/null and b/Resources/Textures/_Null/Structures/warper.rsi/warningtape_down.png differ
diff --git a/Resources/Textures/_Null/Structures/warper.rsi/warningtape_up.png b/Resources/Textures/_Null/Structures/warper.rsi/warningtape_up.png
new file mode 100644
index 00000000000..fedbfddd27b
Binary files /dev/null and b/Resources/Textures/_Null/Structures/warper.rsi/warningtape_up.png differ
diff --git a/Resources/Textures/_TC14/Structures/Walls/stone.rsi/full.png b/Resources/Textures/_TC14/Structures/Walls/stone.rsi/full.png
new file mode 100644
index 00000000000..1432c676f71
Binary files /dev/null and b/Resources/Textures/_TC14/Structures/Walls/stone.rsi/full.png differ
diff --git a/Resources/Textures/_TC14/Structures/Walls/stone.rsi/meta.json b/Resources/Textures/_TC14/Structures/Walls/stone.rsi/meta.json
new file mode 100644
index 00000000000..69097998ac9
--- /dev/null
+++ b/Resources/Textures/_TC14/Structures/Walls/stone.rsi/meta.json
@@ -0,0 +1 @@
+{"version": 1, "size": {"x": 32, "y": 32}, "license": "CC-BY-SA-3.0", "copyright": "https://github.com/vgstation-coders/vgstation13/raw/99cc2ab62d65a3a7b554dc7b21ff5f57c835f973/icons/turf/walls.dmi", "states": [{"name": "full"}, {"name": "stone0", "directions": 4}, {"name": "stone1", "directions": 4}, {"name": "stone2", "directions": 4}, {"name": "stone3", "directions": 4}, {"name": "stone4", "directions": 4}, {"name": "stone5", "directions": 4}, {"name": "stone6", "directions": 4}, {"name": "stone7", "directions": 4}]}
diff --git a/Resources/Textures/_TC14/Structures/Walls/stone.rsi/stone0.png b/Resources/Textures/_TC14/Structures/Walls/stone.rsi/stone0.png
new file mode 100644
index 00000000000..c17ebc0c9e2
Binary files /dev/null and b/Resources/Textures/_TC14/Structures/Walls/stone.rsi/stone0.png differ
diff --git a/Resources/Textures/_TC14/Structures/Walls/stone.rsi/stone1.png b/Resources/Textures/_TC14/Structures/Walls/stone.rsi/stone1.png
new file mode 100644
index 00000000000..8a7f7e774fb
Binary files /dev/null and b/Resources/Textures/_TC14/Structures/Walls/stone.rsi/stone1.png differ
diff --git a/Resources/Textures/_TC14/Structures/Walls/stone.rsi/stone2.png b/Resources/Textures/_TC14/Structures/Walls/stone.rsi/stone2.png
new file mode 100644
index 00000000000..c17ebc0c9e2
Binary files /dev/null and b/Resources/Textures/_TC14/Structures/Walls/stone.rsi/stone2.png differ
diff --git a/Resources/Textures/_TC14/Structures/Walls/stone.rsi/stone3.png b/Resources/Textures/_TC14/Structures/Walls/stone.rsi/stone3.png
new file mode 100644
index 00000000000..8a7f7e774fb
Binary files /dev/null and b/Resources/Textures/_TC14/Structures/Walls/stone.rsi/stone3.png differ
diff --git a/Resources/Textures/_TC14/Structures/Walls/stone.rsi/stone4.png b/Resources/Textures/_TC14/Structures/Walls/stone.rsi/stone4.png
new file mode 100644
index 00000000000..ff6c5e064a1
Binary files /dev/null and b/Resources/Textures/_TC14/Structures/Walls/stone.rsi/stone4.png differ
diff --git a/Resources/Textures/_TC14/Structures/Walls/stone.rsi/stone5.png b/Resources/Textures/_TC14/Structures/Walls/stone.rsi/stone5.png
new file mode 100644
index 00000000000..f77fd38c4f3
Binary files /dev/null and b/Resources/Textures/_TC14/Structures/Walls/stone.rsi/stone5.png differ
diff --git a/Resources/Textures/_TC14/Structures/Walls/stone.rsi/stone6.png b/Resources/Textures/_TC14/Structures/Walls/stone.rsi/stone6.png
new file mode 100644
index 00000000000..ff6c5e064a1
Binary files /dev/null and b/Resources/Textures/_TC14/Structures/Walls/stone.rsi/stone6.png differ
diff --git a/Resources/Textures/_TC14/Structures/Walls/stone.rsi/stone7.png b/Resources/Textures/_TC14/Structures/Walls/stone.rsi/stone7.png
new file mode 100644
index 00000000000..a213f292bed
Binary files /dev/null and b/Resources/Textures/_TC14/Structures/Walls/stone.rsi/stone7.png differ