using System.Diagnostics.CodeAnalysis; using Content.Client.Stylesheets; using Content.Client.UserInterface.Controls; using Content.Shared.Power; using Content.Shared.Rounding; using Robust.Client.AutoGenerated; using Robust.Client.Graphics; using Robust.Client.UserInterface.Controls; using Robust.Client.UserInterface.XAML; using Robust.Shared.Timing; namespace Content.Client.Power.Battery; /// /// Interface control for batteries. /// /// [GenerateTypedNameReferences] public sealed partial class BatteryMenu : FancyWindow { // Cutoff for the ETA time to switch from "~" to ">" and cap out. private const float MaxEtaValueMinutes = 60; // Cutoff where ETA times likely don't make sense and it's better to just say "N/A". private const float NotApplicableEtaHighCutoffMinutes = 1000; private const float NotApplicableEtaLowCutoffMinutes = 0.01f; // Fudge factor to ignore small charge/discharge values, that are likely caused by floating point rounding errors. private const float PrecisionRoundFactor = 100_000; // Colors used for the storage cell bar graphic. private static readonly Color[] StorageColors = [ StyleNano.DangerousRedFore, Color.FromHex("#C49438"), Color.FromHex("#B3BF28"), StyleNano.GoodGreenFore, ]; // StorageColors but dimmed for "off" bars. private static readonly Color[] DimStorageColors = [ DimStorageColor(StorageColors[0]), DimStorageColor(StorageColors[1]), DimStorageColor(StorageColors[2]), DimStorageColor(StorageColors[3]), ]; // Parameters for the sine wave pulsing animations for active power lines in the UI. private static readonly Color ActivePowerLineHighColor = Color.FromHex("#CCC"); private static readonly Color ActivePowerLineLowColor = Color.FromHex("#888"); private const float PowerPulseFactor = 4; // Dependencies [Dependency] private readonly IEntityManager _entityManager = null!; [Dependency] private readonly ILocalizationManager _loc = null!; // Active and inactive style boxes for power lines. // We modify _activePowerLineStyleBox's properties programmatically to implement the pulsing animation. private readonly StyleBoxFlat _activePowerLineStyleBox = new(); private readonly StyleBoxFlat _inactivePowerLineStyleBox = new() { BackgroundColor = Color.FromHex("#555") }; // Style boxes for the storage cell bar graphic. // We modify the properties of these to change the bars' colors. private StyleBoxFlat[] _chargeMeterBoxes; // State for the powerline pulsing animation. private float _powerPulseValue; // State for the storage cell bar graphic and its blinking effect. private float _blinkPulseValue; private bool _blinkPulse; private int _storageLevel; private bool _hasStorageDelta; // The entity that this UI is for. private EntityUid _entity; // Used to avoid sending input events when updating slider values. private bool _suppressSliderEvents; // Events for the BUI to subscribe to. public event Action? OnInBreaker; public event Action? OnOutBreaker; public event Action? OnChargeRate; public event Action? OnDischargeRate; public BatteryMenu() { RobustXamlLoader.Load(this); IoCManager.InjectDependencies(this); InitChargeMeter(); InBreaker.StateChanged += val => OnInBreaker?.Invoke(val); OutBreaker.StateChanged += val => OnOutBreaker?.Invoke(val); ChargeRateSlider.OnValueChanged += _ => { if (!_suppressSliderEvents) OnChargeRate?.Invoke(ChargeRateSlider.Value); }; DischargeRateSlider.OnValueChanged += _ => { if (!_suppressSliderEvents) OnDischargeRate?.Invoke(DischargeRateSlider.Value); }; } public void SetEntity(EntityUid entity) { _entity = entity; this.SetInfoFromEntity(_entityManager, _entity); EntityView.SetEntity(entity); } [MemberNotNull(nameof(_chargeMeterBoxes))] public void InitChargeMeter() { _chargeMeterBoxes = new StyleBoxFlat[StorageColors.Length]; for (var i = StorageColors.Length - 1; i >= 0; i--) { var styleBox = new StyleBoxFlat(); _chargeMeterBoxes[i] = styleBox; for (var j = 0; j < ChargeMeter.Columns; j++) { var control = new PanelContainer { Margin = new Thickness(2), PanelOverride = styleBox, HorizontalExpand = true, VerticalExpand = true, }; ChargeMeter.AddChild(control); } } } public void Update(BatteryBuiState msg) { var inValue = msg.CurrentReceiving; var outValue = msg.CurrentSupply; var storageDelta = inValue - outValue; // Mask rounding errors in power code. if (Math.Abs(storageDelta) < msg.Capacity / PrecisionRoundFactor) storageDelta = 0; // Update power lines based on a ton of parameters. SetPowerLineState(InPowerLine, msg.SupplyingNetworkHasPower); SetPowerLineState(OutPowerLine, msg.LoadingNetworkHasPower); SetPowerLineState(InSecondPowerLine, msg.SupplyingNetworkHasPower && msg.CanCharge); SetPowerLineState(ChargePowerLine, msg.SupplyingNetworkHasPower && msg.CanCharge && storageDelta > 0); SetPowerLineState(PassthroughPowerLine, msg.SupplyingNetworkHasPower && msg.CanCharge && msg.CanDischarge); SetPowerLineState(OutSecondPowerLine, msg.CanDischarge && (msg.Charge > 0 || msg.SupplyingNetworkHasPower && msg.CanCharge)); SetPowerLineState(DischargePowerLine, storageDelta < 0); // Update breakers. InBreaker.IsOn = msg.CanCharge; OutBreaker.IsOn = msg.CanDischarge; // Update various power values. InValue.Text = FormatPower(inValue); OutValue.Text = FormatPower(outValue); PassthroughValue.Text = FormatPower(Math.Min(msg.CurrentReceiving, msg.CurrentSupply)); ChargeMaxValue.Text = FormatPower(msg.MaxChargeRate); DischargeMaxValue.Text = FormatPower(msg.MaxSupply); ChargeCurrentValue.Text = FormatPower(Math.Max(0, storageDelta)); DischargeCurrentValue.Text = FormatPower(Math.Max(0, -storageDelta)); // Update charge/discharge rate sliders. _suppressSliderEvents = true; ChargeRateSlider.MaxValue = msg.MaxMaxChargeRate; ChargeRateSlider.MinValue = msg.MinMaxChargeRate; ChargeRateSlider.Value = msg.MaxChargeRate; DischargeRateSlider.MaxValue = msg.MaxMaxSupply; DischargeRateSlider.MinValue = msg.MinMaxSupply; DischargeRateSlider.Value = msg.MaxSupply; _suppressSliderEvents = false; // Update ETA display. var storageEtaDiff = storageDelta > 0 ? (msg.Capacity - msg.Charge) * (1 / msg.Efficiency) : -msg.Charge; var etaTimeSeconds = storageEtaDiff / storageDelta; var etaTimeMinutes = etaTimeSeconds / 60.0; EtaLabel.Text = _loc.GetString( storageDelta > 0 ? "battery-menu-eta-full" : "battery-menu-eta-empty"); if (!double.IsFinite(etaTimeMinutes) || Math.Abs(etaTimeMinutes) > NotApplicableEtaHighCutoffMinutes || Math.Abs(etaTimeMinutes) < NotApplicableEtaLowCutoffMinutes) { EtaValue.Text = _loc.GetString("battery-menu-eta-value-na"); } else { EtaValue.Text = _loc.GetString( etaTimeMinutes > MaxEtaValueMinutes ? "battery-menu-eta-value-max" : "battery-menu-eta-value", ("minutes", Math.Min(Math.Ceiling(etaTimeMinutes), MaxEtaValueMinutes))); } // Update storage display. StoredPercentageValue.Text = _loc.GetString( "battery-menu-stored-percent-value", ("value", msg.Charge / msg.Capacity)); StoredEnergyValue.Text = _loc.GetString( "battery-menu-stored-energy-value", ("value", msg.Charge)); // Update charge meter. _storageLevel = ContentHelpers.RoundToNearestLevels(msg.Charge, msg.Capacity, _chargeMeterBoxes.Length); _hasStorageDelta = Math.Abs(storageDelta) > 0; } private static Color DimStorageColor(Color color) { var hsv = Color.ToHsv(color); hsv.Z /= 5; return Color.FromHsv(hsv); } private void SetPowerLineState(PanelContainer control, bool value) { control.PanelOverride = value ? _activePowerLineStyleBox : _inactivePowerLineStyleBox; } private string FormatPower(float value) { return _loc.GetString("battery-menu-power-value", ("value", value)); } protected override void FrameUpdate(FrameEventArgs args) { base.FrameUpdate(args); // Pulse power lines. _powerPulseValue += args.DeltaSeconds * PowerPulseFactor; var color = Color.InterpolateBetween( ActivePowerLineLowColor, ActivePowerLineHighColor, MathF.Sin(_powerPulseValue) / 2 + 1); _activePowerLineStyleBox.BackgroundColor = color; // Update storage indicator and blink it. for (var i = 0; i < _chargeMeterBoxes.Length; i++) { var box = _chargeMeterBoxes[i]; if (_storageLevel > i) { // On box.BackgroundColor = StorageColors[i]; } else { box.BackgroundColor = DimStorageColors[i]; } } _blinkPulseValue += args.DeltaSeconds; if (_blinkPulseValue > 1) { _blinkPulseValue -= 1; _blinkPulse ^= true; } // If there is a storage delta (charging or discharging), we want to blink the highest bar. if (_hasStorageDelta) { // If there is no highest bar (UI completely at 0), then blink bar 0. var toBlink = Math.Max(0, _storageLevel - 1); _chargeMeterBoxes[toBlink].BackgroundColor = _blinkPulse ? StorageColors[toBlink] : DimStorageColors[toBlink]; } } }