diff --git a/Content.Client/Backmen/Supermatter/Consoles/SupermatterConsoleBoundUserInterface.cs b/Content.Client/Backmen/Supermatter/Consoles/SupermatterConsoleBoundUserInterface.cs
new file mode 100644
index 00000000000..f6087d35963
--- /dev/null
+++ b/Content.Client/Backmen/Supermatter/Consoles/SupermatterConsoleBoundUserInterface.cs
@@ -0,0 +1,42 @@
+using Content.Shared.Backmen.Supermatter.Consoles;
+
+namespace Content.Client.Backmen.Supermatter.Consoles;
+
+public sealed class SupermatterConsoleBoundUserInterface : BoundUserInterface
+{
+ [ViewVariables]
+ private SupermatterConsoleWindow? _menu;
+
+ public SupermatterConsoleBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey)
+ {
+ }
+
+ protected override void Open()
+ {
+ base.Open();
+
+ _menu = new SupermatterConsoleWindow(this, Owner);
+ _menu.OpenCentered();
+ _menu.OnClose += Close;
+ }
+
+ protected override void UpdateState(BoundUserInterfaceState state)
+ {
+ base.UpdateState(state);
+
+ var castState = (SupermatterConsoleBoundInterfaceState) state;
+ _menu?.UpdateUI(castState.Supermatters, castState.FocusData);
+ }
+
+ public void SendFocusChangeMessage(NetEntity? netEntity) =>
+ SendMessage(new SupermatterConsoleFocusChangeMessage(netEntity));
+
+ protected override void Dispose(bool disposing)
+ {
+ base.Dispose(disposing);
+ if (!disposing)
+ return;
+
+ _menu?.Dispose();
+ }
+}
diff --git a/Content.Client/Backmen/Supermatter/Consoles/SupermatterConsoleWindow.xaml b/Content.Client/Backmen/Supermatter/Consoles/SupermatterConsoleWindow.xaml
new file mode 100644
index 00000000000..f817172483c
--- /dev/null
+++ b/Content.Client/Backmen/Supermatter/Consoles/SupermatterConsoleWindow.xaml
@@ -0,0 +1,34 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Content.Client/Backmen/Supermatter/Consoles/SupermatterConsoleWindow.xaml.cs b/Content.Client/Backmen/Supermatter/Consoles/SupermatterConsoleWindow.xaml.cs
new file mode 100644
index 00000000000..9423cd6dfd8
--- /dev/null
+++ b/Content.Client/Backmen/Supermatter/Consoles/SupermatterConsoleWindow.xaml.cs
@@ -0,0 +1,188 @@
+using System.Diagnostics.CodeAnalysis;
+using System.Linq;
+using Content.Client.Message;
+using Content.Client.UserInterface.Controls;
+using Content.Shared.Backmen.Supermatter.Consoles;
+using Robust.Client.AutoGenerated;
+using Robust.Client.UserInterface;
+using Robust.Client.UserInterface.Controls;
+using Robust.Client.UserInterface.XAML;
+using Robust.Shared.Timing;
+using Robust.Shared.Utility;
+
+namespace Content.Client.Backmen.Supermatter.Consoles;
+
+[GenerateTypedNameReferences]
+public sealed partial class SupermatterConsoleWindow : FancyWindow
+{
+ private readonly IEntityManager _entManager;
+
+ private readonly EntityUid? _owner;
+ private NetEntity? _trackedEntity;
+
+ private SupermatterConsoleEntry[]? _supermatters;
+
+ public event Action? SendFocusChangeMessageAction;
+
+ private bool _autoScrollActive;
+ private bool _autoScrollAwaitsUpdate;
+
+ public SupermatterConsoleWindow(SupermatterConsoleBoundUserInterface userInterface, EntityUid? owner)
+ {
+ RobustXamlLoader.Load(this);
+ _entManager = IoCManager.Resolve();
+
+ _owner = owner;
+ SendFocusChangeMessageAction += userInterface.SendFocusChangeMessage;
+ }
+
+ public void UpdateUI(SupermatterConsoleEntry[] supermatters, SupermatterFocusData? focusData)
+ {
+ if (_owner == null
+ || !_entManager.TryGetComponent(_owner.Value, out var console))
+ return;
+
+ if (_trackedEntity != focusData?.NetEntity)
+ {
+ SendFocusChangeMessageAction?.Invoke(_trackedEntity);
+ focusData = null;
+ }
+
+ _supermatters = supermatters;
+
+ var supermattersCount = _supermatters.Length;
+
+ while (SupermattersTable.ChildCount > supermattersCount)
+ SupermattersTable.RemoveChild(SupermattersTable.GetChild(SupermattersTable.ChildCount - 1));
+
+ for (var index = 0; index < _supermatters.Length; index++)
+ {
+ var entry = _supermatters[index];
+ UpdateUIEntry(entry, index, SupermattersTable, console, focusData);
+ }
+
+ if (supermattersCount == 0)
+ {
+ var label = new RichTextLabel
+ {
+ HorizontalExpand = true,
+ VerticalExpand = true,
+ HorizontalAlignment = HAlignment.Center,
+ VerticalAlignment = VAlignment.Center,
+ };
+
+ label.SetMarkup(Loc.GetString("supermatter-console-window-no-supermatters"));
+ SupermattersTable.AddChild(label);
+ }
+
+ if (_autoScrollAwaitsUpdate)
+ {
+ _autoScrollActive = true;
+ _autoScrollAwaitsUpdate = false;
+ }
+ }
+
+ private void UpdateUIEntry(
+ SupermatterConsoleEntry entry,
+ int index,
+ Control table,
+ SupermatterConsoleComponent console,
+ SupermatterFocusData? focusData = null)
+ {
+ if (index >= table.ChildCount)
+ {
+ var newEntryContainer = new SupermatterEntryContainer(entry.NetEntity);
+
+ newEntryContainer.FocusButton.OnButtonUp += _ =>
+ {
+ _trackedEntity = _trackedEntity == newEntryContainer.NetEntity
+ ? null
+ : newEntryContainer.NetEntity;
+
+ SendFocusChangeMessageAction?.Invoke(_trackedEntity);
+ };
+
+ table.AddChild(newEntryContainer);
+ }
+
+ var tableChild = table.GetChild(index);
+
+ if (tableChild is not SupermatterEntryContainer)
+ {
+ table.RemoveChild(tableChild);
+ UpdateUIEntry(entry, index, table, console, focusData);
+ return;
+ }
+
+ var entryContainer = (SupermatterEntryContainer) tableChild;
+ entryContainer.UpdateEntry(entry, entry.NetEntity == _trackedEntity, focusData);
+ }
+
+ protected override void FrameUpdate(FrameEventArgs args) =>
+ AutoScrollToFocus();
+
+ private void AutoScrollToFocus()
+ {
+ if (!_autoScrollActive)
+ return;
+
+ var scroll = SupermattersTable.Parent as ScrollContainer;
+ if (scroll == null
+ || !TryGetVerticalScrollbar(scroll, out var vScrollbar)
+ || !TryGetNextScrollPosition(out var nextScrollPosition))
+ return;
+
+ vScrollbar.ValueTarget = nextScrollPosition.Value;
+
+ if (MathHelper.CloseToPercent(vScrollbar.Value, vScrollbar.ValueTarget))
+ _autoScrollActive = false;
+ }
+
+ private static bool TryGetVerticalScrollbar(ScrollContainer scroll, [NotNullWhen(true)] out VScrollBar? vScrollBar)
+ {
+ vScrollBar = null;
+
+ foreach (var child in scroll.Children)
+ {
+ if (child is not VScrollBar castChild)
+ continue;
+
+ vScrollBar = castChild;
+ return true;
+ }
+
+ return false;
+ }
+
+ private bool TryGetNextScrollPosition([NotNullWhen(true)] out float? nextScrollPosition)
+ {
+ nextScrollPosition = null;
+
+ var scroll = SupermattersTable.Parent as ScrollContainer;
+ if (scroll == null)
+ return false;
+
+ var container = scroll.Children.ElementAt(0) as BoxContainer;
+ if (container == null || !container.Children.Any())
+ return false;
+
+ if (!container.Children.Any(x => x.Height > 0))
+ return false;
+
+ nextScrollPosition = 0;
+
+ foreach (var control in container.Children)
+ {
+ if (control is not SupermatterEntryContainer entry)
+ continue;
+
+ if (entry.NetEntity == _trackedEntity)
+ return true;
+
+ nextScrollPosition += control.Height;
+ }
+
+ nextScrollPosition = null;
+ return false;
+ }
+}
diff --git a/Content.Client/Backmen/Supermatter/Consoles/SupermatterEntryContainer.xaml b/Content.Client/Backmen/Supermatter/Consoles/SupermatterEntryContainer.xaml
new file mode 100644
index 00000000000..d4dcb80ae48
--- /dev/null
+++ b/Content.Client/Backmen/Supermatter/Consoles/SupermatterEntryContainer.xaml
@@ -0,0 +1,106 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Content.Client/Backmen/Supermatter/Consoles/SupermatterEntryContainer.xaml.cs b/Content.Client/Backmen/Supermatter/Consoles/SupermatterEntryContainer.xaml.cs
new file mode 100644
index 00000000000..d4fa2c6f972
--- /dev/null
+++ b/Content.Client/Backmen/Supermatter/Consoles/SupermatterEntryContainer.xaml.cs
@@ -0,0 +1,238 @@
+using System.Linq;
+using System.Numerics;
+using Content.Client.Atmos.EntitySystems;
+using Content.Client.Stylesheets;
+using Content.Shared.Atmos;
+using Content.Shared.Atmos.Prototypes;
+using Content.Shared.Backmen.Supermatter.Consoles;
+using Robust.Client.AutoGenerated;
+using Robust.Client.Graphics;
+using Robust.Client.ResourceManagement;
+using Robust.Client.UserInterface;
+using Robust.Client.UserInterface.Controls;
+using Robust.Client.UserInterface.XAML;
+
+namespace Content.Client.Backmen.Supermatter.Consoles;
+
+[GenerateTypedNameReferences]
+public sealed partial class SupermatterEntryContainer : BoxContainer
+{
+ public NetEntity NetEntity;
+
+ private readonly IEntityManager _entManager;
+ private readonly Dictionary _engineDictionary;
+
+ public SupermatterEntryContainer(NetEntity uid)
+ {
+ RobustXamlLoader.Load(this);
+
+ _entManager = IoCManager.Resolve();
+ var cache = IoCManager.Resolve();
+
+ NetEntity = uid;
+
+ var normalFont = new VectorFont(cache.GetResource("/Fonts/NotoSansDisplay/NotoSansDisplay-Regular.ttf"), 11);
+ var monoFont = new VectorFont(cache.GetResource("/EngineFonts/NotoSans/NotoSansMono-Regular.ttf"), 10);
+
+ IntegrityLabel.FontOverride = normalFont;
+ PowerLabel.FontOverride = normalFont;
+ RadiationLabel.FontOverride = normalFont;
+ MolesLabel.FontOverride = normalFont;
+ TemperatureLabel.FontOverride = normalFont;
+ TemperatureLimitLabel.FontOverride = normalFont;
+ WasteLabel.FontOverride = normalFont;
+ AbsorptionLabel.FontOverride = normalFont;
+
+ IntegrityBarLabel.FontOverride = monoFont;
+ PowerBarLabel.FontOverride = monoFont;
+ RadiationBarLabel.FontOverride = monoFont;
+ MolesBarLabel.FontOverride = monoFont;
+ TemperatureBarLabel.FontOverride = monoFont;
+ TemperatureLimitBarLabel.FontOverride = monoFont;
+ WasteBarLabel.FontOverride = monoFont;
+ AbsorptionBarLabel.FontOverride = monoFont;
+
+ var red = StyleNano.DangerousRedFore;
+ var orange = StyleNano.ConcerningOrangeFore;
+ var green = StyleNano.GoodGreenFore;
+ var turqoise = Color.FromHex("#009893");
+
+ _engineDictionary = new Dictionary
+ {
+ { "integrity", new EngineBarEntry(IntegrityBarLabel, IntegrityBar, IntegrityBarBorder, 0.9f, 0.1f, red, orange, green) },
+ { "power", new EngineBarEntry(PowerBarLabel, PowerBar, PowerBarBorder, 0.9f, 0.1f, green, orange, red) },
+ { "radiation", new EngineBarEntry(RadiationBarLabel, RadiationBar, RadiationBarBorder, 0.1f, 0.9f, green, orange, red) },
+ { "moles", new EngineBarEntry(MolesBarLabel, MolesBar, MolesBarBorder, 0.5f, 0.5f, green, orange, red) },
+ { "temperature", new EngineBarEntry(TemperatureBarLabel, TemperatureBar, TemperatureBarBorder, 0.5f, 0.5f, turqoise, green, red) },
+ { "waste", new EngineBarEntry(WasteBarLabel, WasteBar, WasteBarBorder, 0.5f, 0.5f, green, orange, red) },
+ };
+ }
+
+ public void UpdateEntry(SupermatterConsoleEntry entry, bool isFocus, SupermatterFocusData? focusData = null)
+ {
+ NetEntity = entry.NetEntity;
+
+ SupermatterNameLabel.Text = Loc.GetString("supermatter-console-window-label-sm", ("name", entry.EntityName));
+ FocusContainer.Visible = isFocus;
+
+ if (!isFocus || focusData == null)
+ return;
+
+ _engineDictionary["integrity"].Value = focusData.Value.Integrity;
+ _engineDictionary["power"].Value = focusData.Value.Power;
+ _engineDictionary["radiation"].Value = focusData.Value.Radiation;
+ _engineDictionary["moles"].Value = focusData.Value.AbsorbedMoles;
+ _engineDictionary["temperature"].Value = focusData.Value.Temperature;
+ _engineDictionary["waste"].Value = focusData.Value.WasteMultiplier;
+
+ var powerBar = _engineDictionary["power"];
+ var powerPrefix = powerBar.Value switch { >= 1000 => "G", >= 1 => "M", _ => "" };
+ var powerMultiplier = powerBar.Value switch { >= 1000 => 0.001, >= 1 => 1, _ => 1000 };
+ powerBar.Label.Text = Loc.GetString("supermatter-console-window-label-power-bar",
+ ("power", (powerBar.Value * powerMultiplier).ToString("0.000")),
+ ("prefix", powerPrefix));
+
+ var temperatureLimit = focusData.Value.TemperatureLimit;
+ TemperatureBar.MaxValue = Math.Max(temperatureLimit, 1f);
+ TemperatureLimitBarLabel.Text = Loc.GetString("supermatter-console-window-label-temperature-bar",
+ ("temperature", temperatureLimit.ToString("0.00")));
+
+ AbsorptionBarLabel.Text = Loc.GetString("supermatter-console-window-label-absorption-bar",
+ ("absorption", focusData.Value.AbsorptionRatio.ToString("0")));
+
+ foreach (var bar in _engineDictionary)
+ {
+ var current = bar.Value;
+ UpdateEngineBar(current.Bar, current.Border, current.Value, current.LeftSize, current.RightSize,
+ current.LeftColor, current.MiddleColor, current.RightColor);
+
+ if (bar.Key == "power")
+ continue;
+
+ current.Label.Text = Loc.GetString("supermatter-console-window-label-" + bar.Key + "-bar",
+ (bar.Key, current.Value.ToString("0.00")));
+ }
+
+ var atmosphereSystem = _entManager.System();
+ var gases = focusData.Value.GasStorage.Keys
+ .Select(atmosphereSystem.GetGas)
+ .OrderByDescending(gas => GetStoredGas(gas, focusData));
+
+ var index = 0;
+ foreach (var gas in gases)
+ {
+ var stored = GetStoredGas(gas, focusData);
+ var value = focusData.Value.AbsorbedMoles > 0f
+ ? stored / focusData.Value.AbsorbedMoles * 100f
+ : 0f;
+
+ UpdateGasBar(index, GasTable, Loc.GetString(gas.Name), gas.Color, value);
+ index++;
+ }
+ }
+
+ ///
+ /// Looks up stored moles using the gas prototype ID string (PR #6808), not a numeric index.
+ ///
+ private static float GetStoredGas(GasPrototype gas, SupermatterFocusData? focusData)
+ {
+ if (focusData is null)
+ return 0f;
+
+ if (!Enum.TryParse(gas.ID, out Gas id))
+ return 0f;
+
+ return focusData.Value.GasStorage.GetValueOrDefault(id);
+ }
+
+ private static void UpdateEngineBar(
+ ProgressBar bar,
+ PanelContainer border,
+ float value,
+ float leftSize,
+ float rightSize,
+ Color leftColor,
+ Color middleColor,
+ Color rightColor)
+ {
+ var clamped = Math.Clamp(value, bar.MinValue, bar.MaxValue);
+ var normalized = clamped / Math.Max(bar.MaxValue, 1f);
+ var leftHsv = Color.ToHsv(leftColor);
+ var middleHsv = Color.ToHsv(middleColor);
+ var rightHsv = Color.ToHsv(rightColor);
+
+ var minColor = new Vector4(0, 0, 0, 0);
+ var maxColor = new Vector4(1, 1, 1, 1);
+ Color finalColor;
+
+ if (normalized <= leftSize)
+ {
+ normalized /= leftSize;
+ var calcColor = Vector4.Lerp(leftHsv, middleHsv, normalized);
+ finalColor = Color.FromHsv(Vector4.Clamp(calcColor, minColor, maxColor));
+ }
+ else
+ {
+ normalized = (normalized - leftSize) / rightSize;
+ var calcColor = Vector4.Lerp(middleHsv, rightHsv, normalized);
+ finalColor = Color.FromHsv(Vector4.Clamp(calcColor, minColor, maxColor));
+ }
+
+ bar.ForegroundStyleBoxOverride ??= new StyleBoxFlat();
+ border.PanelOverride ??= new StyleBoxFlat();
+
+ ((StyleBoxFlat) bar.ForegroundStyleBoxOverride).BackgroundColor = finalColor;
+ ((StyleBoxFlat) border.PanelOverride).BackgroundColor = finalColor;
+ bar.Value = clamped;
+ }
+
+ private void UpdateGasBar(int index, Control table, string name, Color color, float value)
+ {
+ if (index >= table.ChildCount)
+ table.AddChild(new SupermatterGasBarContainer());
+
+ var tableChild = table.GetChild(index);
+
+ if (tableChild is not SupermatterGasBarContainer)
+ {
+ table.RemoveChild(tableChild);
+ UpdateGasBar(index, table, name, color, value);
+ return;
+ }
+
+ ((SupermatterGasBarContainer) tableChild).UpdateEntry(name, color, value);
+ }
+
+ private sealed class EngineBarEntry
+ {
+ public readonly Label Label;
+ public readonly ProgressBar Bar;
+ public readonly PanelContainer Border;
+ public float Value;
+ public readonly float LeftSize;
+ public readonly float RightSize;
+ public readonly Color LeftColor;
+ public readonly Color MiddleColor;
+ public readonly Color RightColor;
+
+ public EngineBarEntry(
+ Label label,
+ ProgressBar bar,
+ PanelContainer border,
+ float leftSize,
+ float rightSize,
+ Color leftColor,
+ Color middleColor,
+ Color rightColor)
+ {
+ Label = label;
+ Bar = bar;
+ Border = border;
+ LeftSize = leftSize;
+ RightSize = rightSize;
+ LeftColor = leftColor;
+ MiddleColor = middleColor;
+ RightColor = rightColor;
+ }
+ }
+}
diff --git a/Content.Client/Backmen/Supermatter/Consoles/SupermatterGasBarContainer.xaml b/Content.Client/Backmen/Supermatter/Consoles/SupermatterGasBarContainer.xaml
new file mode 100644
index 00000000000..1d119ceeab4
--- /dev/null
+++ b/Content.Client/Backmen/Supermatter/Consoles/SupermatterGasBarContainer.xaml
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Content.Client/Backmen/Supermatter/Consoles/SupermatterGasBarContainer.xaml.cs b/Content.Client/Backmen/Supermatter/Consoles/SupermatterGasBarContainer.xaml.cs
new file mode 100644
index 00000000000..88c1e4f6420
--- /dev/null
+++ b/Content.Client/Backmen/Supermatter/Consoles/SupermatterGasBarContainer.xaml.cs
@@ -0,0 +1,36 @@
+using Robust.Client.AutoGenerated;
+using Robust.Client.Graphics;
+using Robust.Client.ResourceManagement;
+using Robust.Client.UserInterface.Controls;
+using Robust.Client.UserInterface.XAML;
+
+namespace Content.Client.Backmen.Supermatter.Consoles;
+
+[GenerateTypedNameReferences]
+public sealed partial class SupermatterGasBarContainer : BoxContainer
+{
+ public SupermatterGasBarContainer()
+ {
+ RobustXamlLoader.Load(this);
+
+ var cache = IoCManager.Resolve();
+ var normalFont = new VectorFont(cache.GetResource("/Fonts/NotoSansDisplay/NotoSansDisplay-Regular.ttf"), 11);
+ var monoFont = new VectorFont(cache.GetResource("/EngineFonts/NotoSans/NotoSansMono-Regular.ttf"), 10);
+
+ GasLabel.FontOverride = normalFont;
+ GasBarLabel.FontOverride = monoFont;
+ }
+
+ public void UpdateEntry(string name, Color color, float value)
+ {
+ GasBar.Value = value;
+ GasLabel.Text = name + ":";
+ GasBarLabel.Text = Loc.GetString("supermatter-console-window-label-gas-bar", ("gas", value.ToString("0.00")));
+
+ GasBar.ForegroundStyleBoxOverride ??= new StyleBoxFlat();
+ GasBarBorder.PanelOverride ??= new StyleBoxFlat();
+
+ ((StyleBoxFlat) GasBar.ForegroundStyleBoxOverride).BackgroundColor = color;
+ ((StyleBoxFlat) GasBarBorder.PanelOverride).BackgroundColor = color;
+ }
+}
diff --git a/Content.IntegrationTests/Tests/Backmen/Supermatter/ReflectorTest.cs b/Content.IntegrationTests/Tests/Backmen/Supermatter/ReflectorTest.cs
new file mode 100644
index 00000000000..f38316c3579
--- /dev/null
+++ b/Content.IntegrationTests/Tests/Backmen/Supermatter/ReflectorTest.cs
@@ -0,0 +1,160 @@
+using System.Numerics;
+using Content.IntegrationTests.Fixtures;
+using Content.Shared.Backmen.Supermatter.Components;
+using Content.Shared.Projectiles;
+using Robust.Shared.GameObjects;
+using Robust.Shared.Map;
+using Robust.Shared.Maths;
+using Robust.Shared.Physics.Components;
+using Robust.Shared.Physics.Systems;
+using Robust.Shared.Prototypes;
+
+namespace Content.IntegrationTests.Tests.Backmen.Supermatter;
+
+///
+/// Directional reflectors redirect emitter bolts from the back and sides, but absorb direct front hits.
+///
+[TestFixture]
+public sealed class ReflectorTest : GameTest
+{
+ private static readonly EntProtoId Reflector = "Reflector";
+ private static readonly EntProtoId EmitterBolt = "EmitterBolt";
+
+ public override PoolSettings PoolSettings => new() { Connected = false, Dirty = true };
+
+ [Test]
+ public async Task FrontHit_IsNotRedirected()
+ {
+ var map = await Pair.CreateTestMap();
+
+ await Server.WaitAssertion(() =>
+ {
+ var entMan = Server.EntMan;
+ var physics = entMan.System();
+ var xformSys = entMan.System();
+
+ var reflector = entMan.SpawnEntity(Reflector, map.MapCoords);
+ xformSys.SetWorldRotation(reflector, Angle.Zero);
+
+ // Hits the output-facing mirror surface.
+ var bolt = entMan.SpawnEntity(EmitterBolt, map.MapCoords);
+ var boltPhysics = entMan.GetComponent(bolt);
+ physics.SetLinearVelocity(bolt, new Vector2(0f, 10f), body: boltPhysics);
+
+ var projectile = entMan.GetComponent(bolt);
+ var attempt = new ProjectileReflectAttemptEvent(bolt, projectile, false);
+ entMan.EventBus.RaiseLocalEvent(reflector, ref attempt);
+
+ Assert.That(attempt.Cancelled, Is.False, "Direct front hits should be absorbed, not redirected.");
+ });
+ }
+
+ [Test]
+ public async Task BackHit_IsRedirectedAlongOutputFace()
+ {
+ var map = await Pair.CreateTestMap();
+
+ await Server.WaitAssertion(() =>
+ {
+ var entMan = Server.EntMan;
+ var physics = entMan.System();
+ var xformSys = entMan.System();
+
+ var reflector = entMan.SpawnEntity(Reflector, map.MapCoords);
+ xformSys.SetWorldRotation(reflector, Angle.Zero);
+
+ // Typical emitter-behind setup: bolt travels through the back and should exit forward.
+ var bolt = entMan.SpawnEntity(EmitterBolt, map.MapCoords);
+ var boltPhysics = entMan.GetComponent(bolt);
+ physics.SetLinearVelocity(bolt, new Vector2(0f, -10f), body: boltPhysics);
+
+ var projectile = entMan.GetComponent(bolt);
+ var attempt = new ProjectileReflectAttemptEvent(bolt, projectile, false);
+ entMan.EventBus.RaiseLocalEvent(reflector, ref attempt);
+
+ Assert.That(attempt.Cancelled, Is.True, "Back hits should be redirected.");
+
+ var redirected = physics.GetMapLinearVelocity(bolt, component: boltPhysics);
+ var outputDir = Angle.Zero.ToWorldVec();
+ Assert.That(Vector2.Dot(Vector2.Normalize(redirected), outputDir), Is.GreaterThan(0.99f));
+ Assert.That(redirected.Length(), Is.EqualTo(10f).Within(0.01f));
+ Assert.That(
+ xformSys.GetWorldRotation(bolt).Degrees,
+ Is.EqualTo((outputDir.ToWorldAngle() + projectile.Angle).Degrees).Within(0.1));
+ Assert.That(projectile.Shooter, Is.EqualTo(reflector));
+ });
+ }
+
+ [Test]
+ public async Task SideHit_IsRedirectedAlongOutputFace()
+ {
+ var map = await Pair.CreateTestMap();
+
+ await Server.WaitAssertion(() =>
+ {
+ var entMan = Server.EntMan;
+ var physics = entMan.System();
+ var xformSys = entMan.System();
+
+ var reflector = entMan.SpawnEntity(Reflector, map.MapCoords);
+ xformSys.SetWorldRotation(reflector, Angle.Zero);
+
+ var bolt = entMan.SpawnEntity(EmitterBolt, map.MapCoords);
+ var boltPhysics = entMan.GetComponent(bolt);
+ physics.SetLinearVelocity(bolt, new Vector2(10f, 0f), body: boltPhysics);
+
+ var projectile = entMan.GetComponent(bolt);
+ var attempt = new ProjectileReflectAttemptEvent(bolt, projectile, false);
+ entMan.EventBus.RaiseLocalEvent(reflector, ref attempt);
+
+ Assert.That(attempt.Cancelled, Is.True, "Side hits should be redirected.");
+
+ var redirected = physics.GetMapLinearVelocity(bolt, component: boltPhysics);
+ var outputDir = Angle.Zero.ToWorldVec();
+ Assert.That(Vector2.Dot(Vector2.Normalize(redirected), outputDir), Is.GreaterThan(0.99f));
+ });
+ }
+
+ [Test]
+ public async Task EmitterBehindReflector_IsRedirectedAlongOutputFace()
+ {
+ var map = await Pair.CreateTestMap();
+
+ await Server.WaitAssertion(() =>
+ {
+ var entMan = Server.EntMan;
+ var physics = entMan.System();
+ var xformSys = entMan.System();
+
+ var reflector = entMan.SpawnEntity(Reflector, map.MapCoords);
+ // Output faces east toward the containment field.
+ xformSys.SetWorldRotation(reflector, Angle.FromDegrees(90));
+
+ var bolt = entMan.SpawnEntity(EmitterBolt, map.MapCoords);
+ var boltPhysics = entMan.GetComponent(bolt);
+ physics.SetLinearVelocity(bolt, new Vector2(10f, 0f), body: boltPhysics);
+
+ var projectile = entMan.GetComponent(bolt);
+ var attempt = new ProjectileReflectAttemptEvent(bolt, projectile, false);
+ entMan.EventBus.RaiseLocalEvent(reflector, ref attempt);
+
+ Assert.That(attempt.Cancelled, Is.True, "Emitter-behind shots should be redirected forward.");
+
+ var redirected = physics.GetMapLinearVelocity(bolt, component: boltPhysics);
+ var outputDir = Angle.FromDegrees(90).ToWorldVec();
+ Assert.That(Vector2.Dot(Vector2.Normalize(redirected), outputDir), Is.GreaterThan(0.99f));
+ });
+ }
+
+ [Test]
+ public async Task ReflectorPrototype_HasDirectionalReflectComponent()
+ {
+ await Server.WaitAssertion(() =>
+ {
+ var entMan = Server.EntMan;
+ var reflector = entMan.SpawnEntity(Reflector, MapCoordinates.Nullspace);
+
+ Assert.That(entMan.HasComponent(reflector), Is.True);
+ });
+ }
+}
diff --git a/Content.Server/Backmen/Supermatter/Consoles/SupermatterConsoleSystem.cs b/Content.Server/Backmen/Supermatter/Consoles/SupermatterConsoleSystem.cs
new file mode 100644
index 00000000000..c4b1deeb4b6
--- /dev/null
+++ b/Content.Server/Backmen/Supermatter/Consoles/SupermatterConsoleSystem.cs
@@ -0,0 +1,258 @@
+using System.Linq;
+using Content.Server.Atmos.EntitySystems;
+using Content.Shared.Atmos;
+using Content.Shared.Backmen.Supermatter.Components;
+using Content.Shared.Backmen.Supermatter.Consoles;
+using Content.Shared.Backmen.Supermatter.Monitor;
+using Content.Shared.Radiation.Components;
+using Robust.Server.GameObjects;
+
+namespace Content.Server.Backmen.Supermatter.Consoles;
+
+public sealed partial class SupermatterConsoleSystem : SharedSupermatterConsoleSystem
+{
+ [Dependency] private UserInterfaceSystem _userInterfaceSystem = default!;
+ [Dependency] private SharedAppearanceSystem _appearance = default!;
+ [Dependency] private AtmosphereSystem _atmosphere = default!;
+
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ SubscribeLocalEvent(OnConsoleInit);
+ SubscribeLocalEvent(OnConsoleParentChanged);
+ SubscribeLocalEvent(OnFocusChangedMessage);
+ SubscribeLocalEvent(OnGridSplit);
+ }
+
+ private void OnConsoleInit(EntityUid uid, SupermatterConsoleComponent component, ComponentInit args) =>
+ InitializeConsole(uid, component);
+
+ private void OnConsoleParentChanged(EntityUid uid, SupermatterConsoleComponent component, EntParentChangedMessage args) =>
+ InitializeConsole(uid, component);
+
+ private void OnFocusChangedMessage(EntityUid uid, SupermatterConsoleComponent component, SupermatterConsoleFocusChangeMessage args) =>
+ component.FocusSupermatter = args.FocusSupermatter;
+
+ private void OnGridSplit(ref GridSplitEvent args)
+ {
+ var allGrids = args.NewGrids.ToList();
+
+ if (!allGrids.Contains(args.Grid))
+ allGrids.Add(args.Grid);
+
+ var query = AllEntityQuery();
+ while (query.MoveNext(out var ent, out var entConsole, out var entXform))
+ {
+ if (entXform.GridUid == null || !allGrids.Contains(entXform.GridUid.Value))
+ continue;
+
+ InitializeConsole(ent, entConsole);
+ }
+ }
+
+ public override void Update(float frameTime)
+ {
+ base.Update(frameTime);
+
+ var supermatterEntriesForEachGrid = new Dictionary();
+
+ var query = AllEntityQuery();
+ while (query.MoveNext(out var ent, out var entConsole, out var entXform))
+ {
+ if (entXform.GridUid == null)
+ continue;
+
+ if (!supermatterEntriesForEachGrid.TryGetValue(entXform.GridUid.Value, out var supermatterEntries))
+ {
+ supermatterEntries = GetSupermatterStateData(entXform.GridUid.Value).ToArray();
+ supermatterEntriesForEachGrid[entXform.GridUid.Value] = supermatterEntries;
+ }
+
+ var highestStatus = SupermatterStatusType.Inactive;
+
+ foreach (var entry in supermatterEntries)
+ {
+ if (entry.EntityStatus > highestStatus)
+ highestStatus = entry.EntityStatus;
+ }
+
+ if (TryComp(ent, out var entAppearance))
+ _appearance.SetData(ent, SupermatterConsoleVisuals.ComputerLayerScreen, (int) highestStatus, entAppearance);
+
+ UpdateUIState(ent, supermatterEntries, entConsole, entXform);
+ }
+ }
+
+ private void UpdateUIState(
+ EntityUid uid,
+ SupermatterConsoleEntry[] supermatterStateData,
+ SupermatterConsoleComponent component,
+ TransformComponent xform)
+ {
+ if (!_userInterfaceSystem.IsUiOpen(uid, SupermatterConsoleUiKey.Key))
+ return;
+
+ if (xform.GridUid == null)
+ return;
+
+ var gridUid = xform.GridUid.Value;
+
+ // ИСПРАВЛЕНИЕ: Полная проверка перед использованием
+ EntityUid? focusEntity = null;
+ if (component.FocusSupermatter != null)
+ {
+ var entity = GetEntity(component.FocusSupermatter.Value);
+ if (Exists(entity))
+ focusEntity = entity;
+ }
+
+ var focusSupermatterData = GetFocusSupermatterData(focusEntity, gridUid);
+
+ _userInterfaceSystem.SetUiState(uid,
+ SupermatterConsoleUiKey.Key,
+ new SupermatterConsoleBoundInterfaceState(supermatterStateData, focusSupermatterData));
+ }
+
+ private List GetSupermatterStateData(EntityUid gridUid)
+ {
+ var supermatterStateData = new List();
+
+ var querySupermatters = AllEntityQuery();
+ while (querySupermatters.MoveNext(out var ent, out var entSupermatter, out var entXform))
+ {
+ if (entXform.GridUid != gridUid || !entXform.Anchored)
+ continue;
+
+ supermatterStateData.Add(new SupermatterConsoleEntry(
+ GetNetEntity(ent),
+ MetaData(ent).EntityName,
+ GetStatus(entSupermatter)));
+ }
+
+ return supermatterStateData;
+ }
+
+ private SupermatterFocusData? GetFocusSupermatterData(EntityUid? focusSupermatter, EntityUid gridUid)
+ {
+ // ИСПРАВЛЕНИЕ: Полная проверка всех условий
+ if (focusSupermatter == null)
+ return null;
+
+ if (!Exists(focusSupermatter.Value))
+ return null;
+
+ var focusSupermatterXform = Transform(focusSupermatter.Value);
+
+ if (!focusSupermatterXform.Anchored)
+ return null;
+
+ if (focusSupermatterXform.GridUid != gridUid)
+ return null;
+
+ if (!TryComp(focusSupermatter.Value, out var sm))
+ return null;
+
+ if (!TryComp(focusSupermatter.Value, out var radiationComp))
+ return null;
+
+ var gasStorage = GetGasStorage(focusSupermatter.Value, sm);
+ var temperature = GetTemperature(focusSupermatter.Value);
+ var wasteMultiplier = GetWasteMultiplier(focusSupermatter.Value, sm);
+ var tempThreshold = Atmospherics.T0C + sm.HeatPenaltyThreshold;
+
+ return new SupermatterFocusData(
+ GetNetEntity(focusSupermatter.Value),
+ GetIntegrity(sm),
+ sm.Power,
+ radiationComp.Intensity,
+ gasStorage.Values.Sum(),
+ temperature,
+ tempThreshold * sm.DynamicHeatResistance,
+ wasteMultiplier,
+ sm.GasEfficiency * 100f,
+ gasStorage);
+ }
+
+ ///
+ /// Returns moles of the given gas around the supermatter, without consuming the mixture.
+ ///
+ public float GetGas(EntityUid uid, Gas gas)
+ {
+ var mixture = _atmosphere.GetContainingMixture(uid, true, true);
+ return mixture?.GetMoles(gas) ?? 0f;
+ }
+
+ private Dictionary GetGasStorage(EntityUid uid, BkmSupermatterComponent sm)
+ {
+ var gasStorage = new Dictionary(sm.GasStorage.Count);
+
+ foreach (var gas in sm.GasStorage.Keys)
+ gasStorage[gas] = GetGas(uid, gas) * sm.GasEfficiency;
+
+ return gasStorage;
+ }
+
+ private float GetTemperature(EntityUid uid)
+ {
+ var mixture = _atmosphere.GetContainingMixture(uid, true, true);
+ return mixture?.Temperature ?? 0f;
+ }
+
+ private float GetWasteMultiplier(EntityUid uid, BkmSupermatterComponent sm)
+ {
+ var mixture = _atmosphere.GetContainingMixture(uid, true, true);
+ if (mixture == null || mixture.TotalMoles <= 0f)
+ return 0.5f;
+
+ var totalMoles = mixture.TotalMoles;
+ var heatModifier = 0f;
+
+ foreach (var (gas, data) in sm.GasDataFields)
+ heatModifier += GetGas(uid, gas) / totalMoles * data.HeatPenalty;
+
+ return Math.Max(heatModifier, 0.5f);
+ }
+
+ public float GetIntegrity(BkmSupermatterComponent sm)
+ {
+ var integrity = sm.Damage / sm.ExplosionPoint;
+ integrity = (float) Math.Round(100 - integrity * 100, 2);
+ return integrity < 0 ? 0 : integrity;
+ }
+
+ public SupermatterStatusType GetStatus(BkmSupermatterComponent sm)
+ {
+ if (sm.Delamming)
+ return SupermatterStatusType.Delaminating;
+
+ var integrity = GetIntegrity(sm);
+
+ if (integrity <= 5 || sm.Damage >= sm.DamageEmergencyThreshold)
+ return SupermatterStatusType.Emergency;
+
+ if (integrity <= 25 || sm.Damage >= sm.DamageWarningThreshold)
+ return SupermatterStatusType.Danger;
+
+ if (integrity <= 50 || sm.Damage > sm.DamageArchived)
+ return SupermatterStatusType.Warning;
+
+ if (sm.Power >= sm.PowerPenaltyThreshold)
+ return SupermatterStatusType.Caution;
+
+ if (sm.Power > 0)
+ return SupermatterStatusType.Normal;
+
+ return SupermatterStatusType.Inactive;
+ }
+
+ private void InitializeConsole(EntityUid uid, SupermatterConsoleComponent component)
+ {
+ // ИСПРАВЛЕНИЕ: Используем TryComp вместо Transform()
+ var xform = Transform(uid);
+ if (xform.GridUid == null)
+ return;
+
+ Dirty(uid, component);
+ }
+}
diff --git a/Content.Server/Projectiles/ProjectileSystem.cs b/Content.Server/Projectiles/ProjectileSystem.cs
index 25d08d62a30..998cac719e3 100644
--- a/Content.Server/Projectiles/ProjectileSystem.cs
+++ b/Content.Server/Projectiles/ProjectileSystem.cs
@@ -56,11 +56,11 @@ private void OnStartCollide(EntityUid uid, ProjectileComponent component, ref St
damageRequired -= _damageableSystem.GetTotalDamage((target, damageableComponent));
damageRequired = FixedPoint2.Max(damageRequired, FixedPoint2.Zero);
}
- var deleted = Deleted(target);
+ // start-backmen: reflector
if (_damageableSystem.TryChangeDamage((target, damageableComponent), ev.Damage, out var damage, component.IgnoreResistances, origin: component.Shooter, seedEntity: uid) && Exists(component.Shooter))
{
- if (!deleted)
+ if (Exists(target))
{
_color.RaiseEffect(Color.Red, new List { target }, Filter.Pvs(target, entityManager: EntityManager));
}
@@ -76,13 +76,14 @@ private void OnStartCollide(EntityUid uid, ProjectileComponent component, ref St
component.ProjectileSpent = true;
}
- if (!deleted)
+ if (Exists(target))
{
_guns.PlayImpactSound(target, damage, component.SoundHit, component.ForceSound);
if (!args.OurBody.LinearVelocity.IsLengthZero())
_sharedCameraRecoil.KickCamera(target, args.OurBody.LinearVelocity.Normalized());
}
+ // end-backmen: reflector
if (component.DeleteOnCollide && component.ProjectileSpent)
QueueDel(uid);
diff --git a/Content.Shared/Backmen/Supermatter/Components/DirectionalReflectComponent.cs b/Content.Shared/Backmen/Supermatter/Components/DirectionalReflectComponent.cs
new file mode 100644
index 00000000000..458da1b04cc
--- /dev/null
+++ b/Content.Shared/Backmen/Supermatter/Components/DirectionalReflectComponent.cs
@@ -0,0 +1,33 @@
+using Content.Shared.Weapons.Reflect;
+using Robust.Shared.Audio;
+using Robust.Shared.GameStates;
+
+namespace Content.Shared.Backmen.Supermatter.Components;
+
+///
+/// Redirects projectiles out through the facing side.
+/// Shots from the back or sides are turned toward the output face; only direct hits into the output face are absorbed.
+///
+[RegisterComponent, NetworkedComponent]
+public sealed partial class DirectionalReflectComponent : Component
+{
+ ///
+ /// Projectile reflective types that this reflector accepts.
+ ///
+ [DataField]
+ public ReflectType Reflects = ReflectType.Energy;
+
+ ///
+ /// Angular half-width of the output face that absorbs shots instead of redirecting them.
+ ///
+ [DataField]
+ public Angle FrontAbsorbAngle = Angle.FromDegrees(45);
+
+ ///
+ /// Sound played when a projectile is redirected.
+ ///
+ [DataField]
+ public SoundSpecifier? SoundOnReflect = new SoundPathSpecifier(
+ "/Audio/Weapons/Guns/Hits/laser_sear_wall.ogg",
+ AudioParams.Default.WithVariation(0.05f));
+}
diff --git a/Content.Shared/Backmen/Supermatter/Consoles/SharedSupermatterConsoleSystem.cs b/Content.Shared/Backmen/Supermatter/Consoles/SharedSupermatterConsoleSystem.cs
new file mode 100644
index 00000000000..d940e411254
--- /dev/null
+++ b/Content.Shared/Backmen/Supermatter/Consoles/SharedSupermatterConsoleSystem.cs
@@ -0,0 +1,5 @@
+namespace Content.Shared.Backmen.Supermatter.Consoles;
+
+public abstract partial class SharedSupermatterConsoleSystem : EntitySystem
+{
+}
diff --git a/Content.Shared/Backmen/Supermatter/Consoles/SupermatterConsoleComponent.cs b/Content.Shared/Backmen/Supermatter/Consoles/SupermatterConsoleComponent.cs
new file mode 100644
index 00000000000..ef82b3c5084
--- /dev/null
+++ b/Content.Shared/Backmen/Supermatter/Consoles/SupermatterConsoleComponent.cs
@@ -0,0 +1,107 @@
+using Content.Shared.Atmos;
+using Content.Shared.Backmen.Supermatter.Monitor;
+using Robust.Shared.GameStates;
+using Robust.Shared.Serialization;
+
+namespace Content.Shared.Backmen.Supermatter.Consoles;
+
+[RegisterComponent, NetworkedComponent]
+[Access(typeof(SharedSupermatterConsoleSystem))]
+public sealed partial class SupermatterConsoleComponent : Component
+{
+ ///
+ /// The current entity of interest (selected via the console UI)
+ ///
+ [ViewVariables]
+ public NetEntity? FocusSupermatter;
+}
+
+[Serializable, NetSerializable]
+public struct SupermatterFocusData
+{
+ public NetEntity NetEntity;
+ public float Integrity;
+ public float Power;
+ public float Radiation;
+ public float AbsorbedMoles;
+ public float Temperature;
+ public float TemperatureLimit;
+ public float WasteMultiplier;
+ public float AbsorptionRatio;
+ public Dictionary GasStorage;
+
+ public SupermatterFocusData(
+ NetEntity netEntity,
+ float integrity,
+ float power,
+ float radiation,
+ float absorbedMoles,
+ float temperature,
+ float temperatureLimit,
+ float wasteMultiplier,
+ float absorptionRatio,
+ Dictionary gasStorage)
+ {
+ NetEntity = netEntity;
+ Integrity = integrity;
+ Power = power;
+ Radiation = radiation;
+ AbsorbedMoles = absorbedMoles;
+ Temperature = temperature;
+ TemperatureLimit = temperatureLimit;
+ WasteMultiplier = wasteMultiplier;
+ AbsorptionRatio = absorptionRatio;
+ GasStorage = gasStorage;
+ }
+}
+
+[Serializable, NetSerializable]
+public sealed class SupermatterConsoleBoundInterfaceState : BoundUserInterfaceState
+{
+ public SupermatterConsoleEntry[] Supermatters;
+ public SupermatterFocusData? FocusData;
+
+ public SupermatterConsoleBoundInterfaceState(SupermatterConsoleEntry[] supermatters, SupermatterFocusData? focusData)
+ {
+ Supermatters = supermatters;
+ FocusData = focusData;
+ }
+}
+
+[Serializable, NetSerializable]
+public struct SupermatterConsoleEntry
+{
+ public NetEntity NetEntity;
+ public string EntityName;
+ public SupermatterStatusType EntityStatus;
+
+ public SupermatterConsoleEntry(NetEntity entity, string entityName, SupermatterStatusType status)
+ {
+ NetEntity = entity;
+ EntityName = entityName;
+ EntityStatus = status;
+ }
+}
+
+[Serializable, NetSerializable]
+public sealed class SupermatterConsoleFocusChangeMessage : BoundUserInterfaceMessage
+{
+ public NetEntity? FocusSupermatter;
+
+ public SupermatterConsoleFocusChangeMessage(NetEntity? focusSupermatter)
+ {
+ FocusSupermatter = focusSupermatter;
+ }
+}
+
+[NetSerializable, Serializable]
+public enum SupermatterConsoleVisuals
+{
+ ComputerLayerScreen,
+}
+
+[Serializable, NetSerializable]
+public enum SupermatterConsoleUiKey
+{
+ Key,
+}
diff --git a/Content.Shared/Backmen/Supermatter/DirectionalReflectSystem.cs b/Content.Shared/Backmen/Supermatter/DirectionalReflectSystem.cs
new file mode 100644
index 00000000000..b9c111687e5
--- /dev/null
+++ b/Content.Shared/Backmen/Supermatter/DirectionalReflectSystem.cs
@@ -0,0 +1,133 @@
+using System.Numerics;
+using Content.Shared.Administration.Logs;
+using Content.Shared.Backmen.Supermatter.Components;
+using Content.Shared.Database;
+using Content.Shared.Popups;
+using Content.Shared.Projectiles;
+using Content.Shared.Tag;
+using Content.Shared.Weapons.Ranged.Components;
+using Content.Shared.Weapons.Reflect;
+using Robust.Shared.Audio.Systems;
+using Robust.Shared.Network;
+using Robust.Shared.Physics.Components;
+using Robust.Shared.Physics.Systems;
+
+namespace Content.Shared.Backmen.Supermatter;
+
+///
+/// Handles directional reflection of emitter bolts and other reflective projectiles.
+///
+public sealed partial class DirectionalReflectSystem : EntitySystem
+{
+ [Dependency] private INetManager _net = default!;
+ [Dependency] private SharedPhysicsSystem _physics = default!;
+ [Dependency] private SharedTransformSystem _transform = default!;
+ [Dependency] private SharedAudioSystem _audio = default!;
+ [Dependency] private SharedPopupSystem _popup = default!;
+ [Dependency] private ISharedAdminLogManager _adminLogger = default!;
+ [Dependency] private TagSystem _tags = default!;
+
+ public const string EmitterBoltTag = "EmitterBolt";
+
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ SubscribeLocalEvent(OnProjectileReflectAttempt);
+ }
+
+ private void OnProjectileReflectAttempt(
+ Entity ent,
+ ref ProjectileReflectAttemptEvent args)
+ {
+ if (args.Cancelled)
+ return;
+
+ if (!TryReflectProjectile(ent, args.ProjUid))
+ return;
+
+ args.Cancelled = true;
+ }
+
+ private bool TryReflectProjectile(Entity reflector, EntityUid projectileUid)
+ {
+ if (!CanReflect(projectileUid, reflector.Comp))
+ return false;
+
+ if (!TryComp(projectileUid, out var physics))
+ return false;
+
+ var velocity = _physics.GetMapLinearVelocity(projectileUid, component: physics);
+ if (velocity.LengthSquared() < 0.001f)
+ return false;
+
+ var outputAngle = _transform.GetWorldRotation(reflector);
+ var outputDir = outputAngle.ToWorldVec().Normalized();
+ var incomingDir = (-velocity).Normalized();
+ var frontAlignment = Vector2.Dot(incomingDir, outputDir);
+
+ // Only shots fired into the output face are absorbed.
+ if (frontAlignment > Math.Cos(reflector.Comp.FrontAbsorbAngle.Theta))
+ return false;
+
+ // Shots from the back or sides are redirected out through the output face.
+ var speed = velocity.Length();
+ var targetMapVelocity = outputDir * speed;
+ var projectileAngle = TryComp(projectileUid, out var projectile)
+ ? projectile.Angle
+ : Angle.Zero;
+ var projectileRotation = outputDir.ToWorldAngle() + projectileAngle;
+
+ // Match gun firing so sprite direction and velocity stay aligned.
+ var finalLinear = physics.LinearVelocity + targetMapVelocity - velocity;
+ _physics.SetLinearVelocity(projectileUid, finalLinear, body: physics);
+ _physics.SetAngularVelocity(projectileUid, 0f, body: physics);
+
+ // Push the bolt past the reflector so collision resolution does not skew the trajectory.
+ var reflectorPos = _transform.GetWorldPosition(reflector);
+ _transform.SetWorldPositionRotation(
+ projectileUid,
+ reflectorPos + outputDir * 0.55f,
+ projectileRotation);
+
+ PlayAudioAndPopup(reflector.Comp, reflector);
+
+ if (projectile != null)
+ {
+ _adminLogger.Add(
+ LogType.BulletHit,
+ LogImpact.Medium,
+ $"{ToPrettyString(reflector)} directionally reflected {ToPrettyString(projectileUid)} from {ToPrettyString(projectile.Weapon)} shot by {projectile.Shooter}");
+
+ projectile.Shooter = reflector;
+ projectile.Weapon = reflector;
+ Dirty(projectileUid, projectile);
+ }
+ else
+ {
+ _adminLogger.Add(
+ LogType.BulletHit,
+ LogImpact.Medium,
+ $"{ToPrettyString(reflector)} directionally reflected {ToPrettyString(projectileUid)}");
+ }
+
+ return true;
+ }
+
+ private bool CanReflect(EntityUid projectile, DirectionalReflectComponent comp)
+ {
+ if (_tags.HasTag(projectile, EmitterBoltTag))
+ return (comp.Reflects & ReflectType.Energy) != 0;
+
+ if (!TryComp(projectile, out var reflective))
+ return false;
+
+ return (comp.Reflects & reflective.Reflective) != 0;
+ }
+
+ private void PlayAudioAndPopup(DirectionalReflectComponent comp, EntityUid reflector)
+ {
+ if (!_net.IsServer)
+ return;
+ }
+}
diff --git a/Content.Shared/Backmen/Supermatter/Monitor/SupermatterStatusType.cs b/Content.Shared/Backmen/Supermatter/Monitor/SupermatterStatusType.cs
new file mode 100644
index 00000000000..a5a2419ee84
--- /dev/null
+++ b/Content.Shared/Backmen/Supermatter/Monitor/SupermatterStatusType.cs
@@ -0,0 +1,16 @@
+using Robust.Shared.Serialization;
+
+namespace Content.Shared.Backmen.Supermatter.Monitor;
+
+[Serializable, NetSerializable]
+public enum SupermatterStatusType : sbyte
+{
+ Error = -1,
+ Inactive = 0,
+ Normal = 1,
+ Caution = 2,
+ Warning = 3,
+ Danger = 4,
+ Emergency = 5,
+ Delaminating = 6,
+}
diff --git a/Resources/Locale/en-US/backmen/supermatter/supermatter-console.ftl b/Resources/Locale/en-US/backmen/supermatter/supermatter-console.ftl
new file mode 100644
index 00000000000..fbf1d4fbfec
--- /dev/null
+++ b/Resources/Locale/en-US/backmen/supermatter/supermatter-console.ftl
@@ -0,0 +1,44 @@
+supermatter-console-window-title = Supermatter Monitoring Console
+supermatter-console-window-station-name = [color=white][font size=14]{$stationName}[/font][/color]
+supermatter-console-window-unknown-location = Unknown location
+supermatter-console-window-no-supermatters = [font size=16][color=white]No supermatter detected[/font]
+
+supermatter-console-window-label-sm = {CAPITALIZE($name)}
+
+supermatter-console-window-label-alert-types = Supermatter status:
+supermatter-console-window-error-status = Error
+supermatter-console-window-inactive-status = Inactive
+supermatter-console-window-normal-status = Normal
+supermatter-console-window-caution-status = Caution
+supermatter-console-window-warning-status = Warning
+supermatter-console-window-danger-status = Danger
+supermatter-console-window-emergency-status = Emergency
+supermatter-console-window-delaminating-status = Delaminating
+
+supermatter-console-window-label-integrity = Integrity:
+supermatter-console-window-label-integrity-bar = {$integrity}%
+
+supermatter-console-window-label-power = Internal Energy:
+supermatter-console-window-label-power-bar = {$power} {$prefix}eV
+
+supermatter-console-window-label-radiation = Radiation Emission:
+supermatter-console-window-label-radiation-bar = {$radiation} rads
+
+supermatter-console-window-label-moles = Absorbed Moles:
+supermatter-console-window-label-moles-bar = {$moles} Moles
+
+supermatter-console-window-label-temperature = Temperature:
+supermatter-console-window-label-temperature-limit = Temperature Limit:
+supermatter-console-window-label-temperature-bar = {$temperature} K
+
+supermatter-console-window-label-waste = Waste Multiplier:
+supermatter-console-window-label-waste-bar = {$waste} x
+
+supermatter-console-window-label-absorption = Absorption Ratio:
+supermatter-console-window-label-absorption-bar = {$absorption}%
+
+supermatter-console-window-label-gas = Unknown gas
+supermatter-console-window-label-gas-bar = {$gas}%
+
+supermatter-console-window-flavor-left = ⚠ Do not approach the crystal
+supermatter-console-window-flavor-right = v1.1
diff --git a/Resources/Locale/en-US/backmen/supermatter/supermatter.ftl b/Resources/Locale/en-US/backmen/supermatter/supermatter.ftl
index 1a30affefbc..de312c2699a 100644
--- a/Resources/Locale/en-US/backmen/supermatter/supermatter.ftl
+++ b/Resources/Locale/en-US/backmen/supermatter/supermatter.ftl
@@ -7,3 +7,12 @@ supermatter-safe-alert = Crystalline hyperstructure returning to safe operating
supermatter-delamination-overmass = The Supermatter has Reached Critical Mass Falure. Singularity formation Imminent
supermatter-delamination-default = The Supermatter has Reached Critical Integrity Falure. Emergency Causality Destabilization Field has been Activated.
supermatter-seconds-before-delam = { $Seconds } Seconds Remain Before Delamination.
+
+ent-Reflector = reflector
+ .desc = An angled mirror that redirects emitter bolts from the back and sides out through its facing direction.
+
+ent-ReflectorFrame = reflector frame
+ .desc = An unfinished reflector assembly. Anchor it, then add a diamond and reinforced glass.
+
+construction-reflector = reflector
+ .desc = An angled mirror for redirecting emitter bolts. Requires 15 steel, 1 diamond, and 5 reinforced glass.
diff --git a/Resources/Locale/en-US/ss14-ru/prototypes/backmen/entities/structures/machines/computers/supermatter_console.ftl b/Resources/Locale/en-US/ss14-ru/prototypes/backmen/entities/structures/machines/computers/supermatter_console.ftl
new file mode 100644
index 00000000000..e041bb3a53b
--- /dev/null
+++ b/Resources/Locale/en-US/ss14-ru/prototypes/backmen/entities/structures/machines/computers/supermatter_console.ftl
@@ -0,0 +1,8 @@
+ent-ComputerSupermatter = supermatter monitoring console
+ .desc = Used to monitor the status of supermatter crystals.
+
+ent-SupermatterComputerCircuitboard = supermatter monitoring console board
+ .desc = A computer printed circuit board for a supermatter monitoring console.
+
+ent-HandheldSupermatterConsole = handheld supermatter console
+ .desc = A portable console that lets the user monitor supermatter crystal status.
diff --git a/Resources/Locale/ru-RU/backmen/supermatter/supermatter-console.ftl b/Resources/Locale/ru-RU/backmen/supermatter/supermatter-console.ftl
new file mode 100644
index 00000000000..fea6b845f07
--- /dev/null
+++ b/Resources/Locale/ru-RU/backmen/supermatter/supermatter-console.ftl
@@ -0,0 +1,44 @@
+supermatter-console-window-title = Консоль мониторинга суперматерии
+supermatter-console-window-station-name = [color=white][font size=14]{$stationName}[/font][/color]
+supermatter-console-window-unknown-location = Неизвестное местоположение
+supermatter-console-window-no-supermatters = [font size=16][color=white]Суперматерия не обнаружена[/font]
+
+supermatter-console-window-label-sm = {CAPITALIZE($name)}
+
+supermatter-console-window-label-alert-types = Статус суперматерии:
+supermatter-console-window-error-status = Ошибка
+supermatter-console-window-inactive-status = Неактивна
+supermatter-console-window-normal-status = Норма
+supermatter-console-window-caution-status = Внимание
+supermatter-console-window-warning-status = Предупреждение
+supermatter-console-window-danger-status = Опасность
+supermatter-console-window-emergency-status = Авария
+supermatter-console-window-delaminating-status = Деламинация
+
+supermatter-console-window-label-integrity = Целостность:
+supermatter-console-window-label-integrity-bar = {$integrity}%
+
+supermatter-console-window-label-power = Внутренняя энергия:
+supermatter-console-window-label-power-bar = {$power} {$prefix}eV
+
+supermatter-console-window-label-radiation = Излучение:
+supermatter-console-window-label-radiation-bar = {$radiation} рад
+
+supermatter-console-window-label-moles = Поглощённые моли:
+supermatter-console-window-label-moles-bar = {$moles} моль
+
+supermatter-console-window-label-temperature = Температура:
+supermatter-console-window-label-temperature-limit = Лимит температуры:
+supermatter-console-window-label-temperature-bar = {$temperature} K
+
+supermatter-console-window-label-waste = Множитель отходов:
+supermatter-console-window-label-waste-bar = {$waste} x
+
+supermatter-console-window-label-absorption = Коэффициент поглощения:
+supermatter-console-window-label-absorption-bar = {$absorption}%
+
+supermatter-console-window-label-gas = Неизвестный газ
+supermatter-console-window-label-gas-bar = {$gas}%
+
+supermatter-console-window-flavor-left = ⚠ Не приближайтесь к кристаллу
+supermatter-console-window-flavor-right = v1.1
diff --git a/Resources/Locale/ru-RU/ss14-ru/prototypes/_backmen/entities/supermatter/reflector.ftl b/Resources/Locale/ru-RU/ss14-ru/prototypes/_backmen/entities/supermatter/reflector.ftl
new file mode 100644
index 00000000000..491664efaae
--- /dev/null
+++ b/Resources/Locale/ru-RU/ss14-ru/prototypes/_backmen/entities/supermatter/reflector.ftl
@@ -0,0 +1,8 @@
+ent-Reflector = отражатель
+ .desc = Угловое зеркало, перенаправляющее болты эмиттера с тыла и боков через лицевую сторону. Говорят что первые версии были сделаны из холодильника СССП.
+
+ent-ReflectorFrame = каркас отражателя
+ .desc = Незавершённая сборка отражателя. Закрепите на полу, затем добавьте алмаз и укреплённое стекло.
+
+construction-reflector = отражатель
+ .desc = Угловое зеркало для перенаправления болтов эмиттера. Требуется 15 стали, 1 алмаз и 5 листов укреплённого стекла.
diff --git a/Resources/Locale/ru-RU/ss14-ru/prototypes/backmen/entities/structures/machines/computers/supermatter_console.ftl b/Resources/Locale/ru-RU/ss14-ru/prototypes/backmen/entities/structures/machines/computers/supermatter_console.ftl
new file mode 100644
index 00000000000..a1ae85bc105
--- /dev/null
+++ b/Resources/Locale/ru-RU/ss14-ru/prototypes/backmen/entities/structures/machines/computers/supermatter_console.ftl
@@ -0,0 +1,8 @@
+ent-ComputerSupermatter = консоль мониторинга суперматерии
+ .desc = Используется для мониторинга состояния кристаллов суперматерии.
+
+ent-SupermatterComputerCircuitboard = плата консоли мониторинга суперматерии
+ .desc = Печатная плата компьютера для консоли мониторинга суперматерии.
+
+ent-HandheldSupermatterConsole = портативная консоль суперматерии
+ .desc = Портативная консоль для мониторинга состояния кристалла суперматерии.
diff --git a/Resources/Prototypes/Catalog/Fills/Lockers/heads.yml b/Resources/Prototypes/Catalog/Fills/Lockers/heads.yml
index 68312049a8e..d6b85707eff 100644
--- a/Resources/Prototypes/Catalog/Fills/Lockers/heads.yml
+++ b/Resources/Prototypes/Catalog/Fills/Lockers/heads.yml
@@ -176,6 +176,8 @@
- id: ClothingHeadCage # backmen edit
- id: PrinterDocFlatpack # Corvax-Printer
- id: SupermatterFlatpack # backmen edit
+ - id: SupermatterComputerCircuitboard # backmen: supermatter console
+ - id: HandheldSupermatterConsole # backmen: supermatter console
- id: MetalFoamGrenade
# Hardsuit table, used for suit storage as well
diff --git a/Resources/Prototypes/_Backmen/Entities/Structures/Machines/Computers/supermatter_console.yml b/Resources/Prototypes/_Backmen/Entities/Structures/Machines/Computers/supermatter_console.yml
new file mode 100644
index 00000000000..40793ceaff1
--- /dev/null
+++ b/Resources/Prototypes/_Backmen/Entities/Structures/Machines/Computers/supermatter_console.yml
@@ -0,0 +1,94 @@
+# Supermatter monitoring console — ported from Goob-Station PR #5788 / #6808
+
+- type: entity
+ parent: BaseComputerAiAccess
+ id: ComputerSupermatter
+ name: supermatter monitoring console
+ description: Used to monitor the status of supermatter crystals.
+ components:
+ - type: Computer
+ board: SupermatterComputerCircuitboard
+ - type: Sprite
+ sprite: Backmen/Supermatter/computers.rsi
+ layers:
+ - map: ["computerLayerBody"]
+ state: computer
+ - map: ["computerLayerKeyboard"]
+ state: generic_keyboard
+ - map: ["computerLayerScreen"]
+ state: supermatter-0
+ - map: ["computerLayerKeys"]
+ state: supermatter_keys
+ - map: [ "enum.WiresVisualLayers.MaintenancePanel" ]
+ state: generic_panel_open
+ - type: PointLight
+ radius: 1.5
+ energy: 1.6
+ color: "#0f704b"
+ - type: GenericVisualizer
+ visuals:
+ enum.ComputerVisuals.Powered:
+ computerLayerScreen:
+ True: { visible: true, shader: unshaded }
+ False: { visible: false }
+ computerLayerKeys:
+ True: { visible: true, shader: unshaded }
+ False: { visible: true, shader: shaded }
+ enum.SupermatterConsoleVisuals.ComputerLayerScreen:
+ computerLayerScreen:
+ 0: { state: supermatter-0 }
+ 1: { state: supermatter-1 }
+ 2: { state: supermatter-2 }
+ 3: { state: supermatter-3 }
+ 4: { state: supermatter-4 }
+ 5: { state: supermatter-5 }
+ 6: { state: supermatter-6 }
+ enum.WiresVisuals.MaintenancePanelState:
+ enum.WiresVisualLayers.MaintenancePanel:
+ True: { visible: false }
+ False: { visible: true }
+ - type: SupermatterConsole
+ - type: ActivatableUI
+ singleUser: true
+ key: enum.SupermatterConsoleUiKey.Key
+ - type: UserInterface
+ interfaces:
+ enum.SupermatterConsoleUiKey.Key:
+ type: SupermatterConsoleBoundUserInterface
+ enum.WiresUiKey.Key:
+ type: WiresBoundUserInterface
+
+- type: entity
+ parent: BaseComputerCircuitboard
+ id: SupermatterComputerCircuitboard
+ name: supermatter monitoring console board
+ description: A computer printed circuit board for a supermatter monitoring console.
+ components:
+ - type: Sprite
+ state: cpu_engineering
+ - type: ComputerBoard
+ prototype: ComputerSupermatter
+
+- type: entity
+ id: HandheldSupermatterConsole
+ name: handheld supermatter console
+ description: A portable console that lets the user monitor supermatter crystal status.
+ parent: BaseHandheldComputer
+ components:
+ - type: Sprite
+ sprite: Backmen/Objects/Devices/tablets.rsi
+ layers:
+ - state: tablet
+ - state: supermatterconsole
+ shader: unshaded
+ - type: ActivatableUI
+ inHandsOnly: true
+ singleUser: true
+ key: enum.SupermatterConsoleUiKey.Key
+ - type: SupermatterConsole
+ - type: UserInterface
+ interfaces:
+ enum.SupermatterConsoleUiKey.Key:
+ type: SupermatterConsoleBoundUserInterface
+ - type: Item
+ size: Small
diff --git a/Resources/Prototypes/_Backmen/Entities/Supermatter/reflector.yml b/Resources/Prototypes/_Backmen/Entities/Supermatter/reflector.yml
new file mode 100644
index 00000000000..6698f381383
--- /dev/null
+++ b/Resources/Prototypes/_Backmen/Entities/Supermatter/reflector.yml
@@ -0,0 +1,93 @@
+- type: entity
+ id: ReflectorFrame
+ name: reflector frame
+ description: An unfinished reflector assembly. Anchor it, then add a diamond and reinforced glass.
+ parent: BaseStructureDynamic
+ placement:
+ mode: SnapgridCenter
+ components:
+ - type: Sprite
+ sprite: Backmen/Supermatter/reflectorframe.rsi
+ state: reflector
+ - type: InteractionOutline
+ - type: Rotatable
+ - type: Construction
+ graph: Reflector
+ node: frame
+ - type: Damageable
+ damageModifierSet: StructuralMetallic
+ - type: Injurable
+ damageContainer: StructuralInorganic
+ - type: Destructible
+ thresholds:
+ - trigger:
+ !type:DamageTrigger
+ damage: 150
+ behaviors:
+ - !type:SpawnEntitiesBehavior
+ spawn:
+ SheetSteel1:
+ min: 10
+ max: 15
+ - !type:DoActsBehavior
+ acts: [ "Destruction" ]
+
+- type: entity
+ id: Reflector
+ name: reflector
+ description: An angled mirror that redirects emitter bolts from the back and sides out through its facing direction. It is said that the first versions were made from a CSSP refrigerator.
+ parent: BaseStructure
+ placement:
+ mode: SnapgridCenter
+ components:
+ - type: Sprite
+ sprite: Backmen/Supermatter/reflector.rsi
+ state: reflector
+ - type: InteractionOutline
+ - type: Rotatable
+ - type: Anchorable
+ - type: Appearance
+ - type: Construction
+ graph: Reflector
+ node: reflector
+ - type: DirectionalReflect
+ reflects: Energy
+ frontAbsorbAngle: 45
+ - type: Damageable
+ damageModifierSet: StructuralMetallicStrong
+ - type: Injurable
+ damageContainer: StructuralInorganic
+ - type: Destructible
+ thresholds:
+ - trigger:
+ !type:DamageTrigger
+ damage: 300
+ behaviors:
+ - !type:PlaySoundBehavior
+ sound:
+ collection: MetalBreak
+ - !type:SpawnEntitiesBehavior
+ spawn:
+ SheetSteel1:
+ min: 10
+ max: 15
+ MaterialDiamond1:
+ min: 1
+ max: 1
+ SheetRGlass1:
+ min: 3
+ max: 5
+ - !type:DoActsBehavior
+ acts: [ "Destruction" ]
+ - type: Fixtures
+ fixtures:
+ fix1:
+ shape:
+ !type:PhysShapeAabb
+ bounds: "-0.49,-0.49,0.49,0.49"
+ hard: true
+ density: 1000
+ mask:
+ - MachineMask
+ layer:
+ - MachineLayer
diff --git a/Resources/Prototypes/_Backmen/Recipes/Construction/reflector.yml b/Resources/Prototypes/_Backmen/Recipes/Construction/reflector.yml
new file mode 100644
index 00000000000..f3dc9c7a587
--- /dev/null
+++ b/Resources/Prototypes/_Backmen/Recipes/Construction/reflector.yml
@@ -0,0 +1,65 @@
+- type: constructionGraph
+ id: Reflector
+ start: start
+ graph:
+ - node: start
+ edges:
+ - to: frame
+ completed:
+ - !type:SnapToGrid
+ southRotation: true
+ steps:
+ - material: Steel
+ amount: 15
+ doAfter: 10
+
+ - node: frame
+ entity: ReflectorFrame
+ edges:
+ - to: reflector
+ conditions:
+ - !type:EntityAnchored
+ steps:
+ - material: Diamond
+ amount: 1
+ doAfter: 3
+ - material: ReinforcedGlass
+ amount: 5
+ doAfter: 5
+ - to: start
+ completed:
+ - !type:SpawnPrototype
+ prototype: SheetSteel1
+ amount: 15
+ - !type:DeleteEntity
+ steps:
+ - tool: Welding
+ doAfter: 5
+
+ - node: reflector
+ entity: Reflector
+ edges:
+ - to: frame
+ completed:
+ - !type:SpawnPrototype
+ prototype: MaterialDiamond1
+ amount: 1
+ - !type:SpawnPrototype
+ prototype: SheetRGlass1
+ amount: 5
+ steps:
+ - tool: Welding
+ doAfter: 8
+
+- type: construction
+ id: Reflector
+ graph: Reflector
+ startNode: start
+ targetNode: reflector
+ category: construction-category-structures
+ objectType: Structure
+ placementMode: SnapgridCenter
+ canRotate: true
+ canBuildInImpassable: false
+ conditions:
+ - !type:TileNotBlocked
diff --git a/Resources/Textures/Backmen/Objects/Devices/tablets.rsi/meta.json b/Resources/Textures/Backmen/Objects/Devices/tablets.rsi/meta.json
new file mode 100644
index 00000000000..a5ac58d0b39
--- /dev/null
+++ b/Resources/Textures/Backmen/Objects/Devices/tablets.rsi/meta.json
@@ -0,0 +1,10 @@
+{
+ "version": 1,
+ "license": "CC-BY-SA-3.0",
+ "copyright": "Base tablet from Space Station 14. Supermatter console icon from Goob-Station PR #5788.",
+ "size": { "x": 32, "y": 32 },
+ "states": [
+ { "name": "tablet" },
+ { "name": "supermatterconsole" }
+ ]
+}
diff --git a/Resources/Textures/Backmen/Objects/Devices/tablets.rsi/supermatterconsole.png b/Resources/Textures/Backmen/Objects/Devices/tablets.rsi/supermatterconsole.png
new file mode 100644
index 00000000000..c3350e8f335
Binary files /dev/null and b/Resources/Textures/Backmen/Objects/Devices/tablets.rsi/supermatterconsole.png differ
diff --git a/Resources/Textures/Backmen/Objects/Devices/tablets.rsi/tablet.png b/Resources/Textures/Backmen/Objects/Devices/tablets.rsi/tablet.png
new file mode 100644
index 00000000000..417d44fbcc0
Binary files /dev/null and b/Resources/Textures/Backmen/Objects/Devices/tablets.rsi/tablet.png differ
diff --git a/Resources/Textures/Backmen/Supermatter/computers.rsi/computer.png b/Resources/Textures/Backmen/Supermatter/computers.rsi/computer.png
new file mode 100644
index 00000000000..c608b2bb4d9
Binary files /dev/null and b/Resources/Textures/Backmen/Supermatter/computers.rsi/computer.png differ
diff --git a/Resources/Textures/Backmen/Supermatter/computers.rsi/generic_keyboard.png b/Resources/Textures/Backmen/Supermatter/computers.rsi/generic_keyboard.png
new file mode 100644
index 00000000000..24428eb4e36
Binary files /dev/null and b/Resources/Textures/Backmen/Supermatter/computers.rsi/generic_keyboard.png differ
diff --git a/Resources/Textures/Backmen/Supermatter/computers.rsi/generic_panel_open.png b/Resources/Textures/Backmen/Supermatter/computers.rsi/generic_panel_open.png
new file mode 100644
index 00000000000..ac7f9f66414
Binary files /dev/null and b/Resources/Textures/Backmen/Supermatter/computers.rsi/generic_panel_open.png differ
diff --git a/Resources/Textures/Backmen/Supermatter/computers.rsi/meta.json b/Resources/Textures/Backmen/Supermatter/computers.rsi/meta.json
new file mode 100644
index 00000000000..651e5dbcea8
--- /dev/null
+++ b/Resources/Textures/Backmen/Supermatter/computers.rsi/meta.json
@@ -0,0 +1,73 @@
+{
+ "version": 1,
+ "license": "CC-BY-SA-3.0",
+ "copyright": "Generic sprites taken from tgstation at commit https://github.com/tgstation/tgstation/commit/bd6873fd4dd6a61d7e46f1d75cd4d90f64c40894. generic_panel_open made by Errant, commit https://github.com/space-wizards/space-station-14/pull/32273. Supermatter computer sprites by AftrLite(Github).",
+ "size": {
+ "x": 32,
+ "y": 32
+ },
+ "states": [
+ {
+ "name": "computer",
+ "directions": 4
+ },
+ {
+ "name": "generic_keyboard",
+ "directions": 4
+ },
+ {
+ "name": "generic_panel_open",
+ "directions": 4
+ },
+ {
+ "name": "supermatter_keys",
+ "directions": 4
+ },
+ {
+ "name": "supermatter-0",
+ "directions": 4
+ },
+ {
+ "name": "supermatter-1",
+ "directions": 4
+ },
+ {
+ "name": "supermatter-2",
+ "directions": 4
+ },
+ {
+ "name": "supermatter-3",
+ "directions": 4
+ },
+ {
+ "name": "supermatter-4",
+ "directions": 4
+ },
+ {
+ "name": "supermatter-5",
+ "directions": 4
+ },
+ {
+ "name": "supermatter-6",
+ "directions": 4,
+ "delays": [
+ [
+ 0.1,
+ 0.1
+ ],
+ [
+ 0.1,
+ 0.1
+ ],
+ [
+ 0.1,
+ 0.1
+ ],
+ [
+ 0.1,
+ 0.1
+ ]
+ ]
+ }
+ ]
+}
diff --git a/Resources/Textures/Backmen/Supermatter/computers.rsi/supermatter-0.png b/Resources/Textures/Backmen/Supermatter/computers.rsi/supermatter-0.png
new file mode 100644
index 00000000000..d8301a40231
Binary files /dev/null and b/Resources/Textures/Backmen/Supermatter/computers.rsi/supermatter-0.png differ
diff --git a/Resources/Textures/Backmen/Supermatter/computers.rsi/supermatter-1.png b/Resources/Textures/Backmen/Supermatter/computers.rsi/supermatter-1.png
new file mode 100644
index 00000000000..b74452db524
Binary files /dev/null and b/Resources/Textures/Backmen/Supermatter/computers.rsi/supermatter-1.png differ
diff --git a/Resources/Textures/Backmen/Supermatter/computers.rsi/supermatter-2.png b/Resources/Textures/Backmen/Supermatter/computers.rsi/supermatter-2.png
new file mode 100644
index 00000000000..8096e8fc4ae
Binary files /dev/null and b/Resources/Textures/Backmen/Supermatter/computers.rsi/supermatter-2.png differ
diff --git a/Resources/Textures/Backmen/Supermatter/computers.rsi/supermatter-3.png b/Resources/Textures/Backmen/Supermatter/computers.rsi/supermatter-3.png
new file mode 100644
index 00000000000..fac6045054a
Binary files /dev/null and b/Resources/Textures/Backmen/Supermatter/computers.rsi/supermatter-3.png differ
diff --git a/Resources/Textures/Backmen/Supermatter/computers.rsi/supermatter-4.png b/Resources/Textures/Backmen/Supermatter/computers.rsi/supermatter-4.png
new file mode 100644
index 00000000000..4da4dc822c9
Binary files /dev/null and b/Resources/Textures/Backmen/Supermatter/computers.rsi/supermatter-4.png differ
diff --git a/Resources/Textures/Backmen/Supermatter/computers.rsi/supermatter-5.png b/Resources/Textures/Backmen/Supermatter/computers.rsi/supermatter-5.png
new file mode 100644
index 00000000000..21a0631e777
Binary files /dev/null and b/Resources/Textures/Backmen/Supermatter/computers.rsi/supermatter-5.png differ
diff --git a/Resources/Textures/Backmen/Supermatter/computers.rsi/supermatter-6.png b/Resources/Textures/Backmen/Supermatter/computers.rsi/supermatter-6.png
new file mode 100644
index 00000000000..15d55084a87
Binary files /dev/null and b/Resources/Textures/Backmen/Supermatter/computers.rsi/supermatter-6.png differ
diff --git a/Resources/Textures/Backmen/Supermatter/computers.rsi/supermatter_keys.png b/Resources/Textures/Backmen/Supermatter/computers.rsi/supermatter_keys.png
new file mode 100644
index 00000000000..3fec5a800b7
Binary files /dev/null and b/Resources/Textures/Backmen/Supermatter/computers.rsi/supermatter_keys.png differ
diff --git a/Resources/Textures/Backmen/Supermatter/reflector.rsi/meta.json b/Resources/Textures/Backmen/Supermatter/reflector.rsi/meta.json
new file mode 100644
index 00000000000..af3ad32623a
--- /dev/null
+++ b/Resources/Textures/Backmen/Supermatter/reflector.rsi/meta.json
@@ -0,0 +1,15 @@
+{
+ "version": 1,
+ "license": "CC-BY-SA-3.0",
+ "copyright": "gttedmi and kesfox15",
+ "size": {
+ "x": 32,
+ "y": 32
+ },
+ "states": [
+ {
+ "name": "reflector",
+ "directions": 4
+ }
+ ]
+}
diff --git a/Resources/Textures/Backmen/Supermatter/reflector.rsi/reflector.png b/Resources/Textures/Backmen/Supermatter/reflector.rsi/reflector.png
new file mode 100644
index 00000000000..c64fd1d0c51
Binary files /dev/null and b/Resources/Textures/Backmen/Supermatter/reflector.rsi/reflector.png differ
diff --git a/Resources/Textures/Backmen/Supermatter/reflectorframe.rsi/meta.json b/Resources/Textures/Backmen/Supermatter/reflectorframe.rsi/meta.json
new file mode 100644
index 00000000000..af3ad32623a
--- /dev/null
+++ b/Resources/Textures/Backmen/Supermatter/reflectorframe.rsi/meta.json
@@ -0,0 +1,15 @@
+{
+ "version": 1,
+ "license": "CC-BY-SA-3.0",
+ "copyright": "gttedmi and kesfox15",
+ "size": {
+ "x": 32,
+ "y": 32
+ },
+ "states": [
+ {
+ "name": "reflector",
+ "directions": 4
+ }
+ ]
+}
diff --git a/Resources/Textures/Backmen/Supermatter/reflectorframe.rsi/reflector.png b/Resources/Textures/Backmen/Supermatter/reflectorframe.rsi/reflector.png
new file mode 100644
index 00000000000..459b3b1bb98
Binary files /dev/null and b/Resources/Textures/Backmen/Supermatter/reflectorframe.rsi/reflector.png differ
diff --git a/Resources/Textures/Interface/Supermatter/supermatter.png b/Resources/Textures/Interface/Supermatter/supermatter.png
new file mode 100644
index 00000000000..e6abc877170
Binary files /dev/null and b/Resources/Textures/Interface/Supermatter/supermatter.png differ