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