using System.Linq; using Content.Client.UserInterface.Controls; using Content.Client._NF.Shipyard.BUI; using Content.Shared._NF.Bank; using Content.Shared._NF.Shipyard.BUI; using Content.Shared._NF.Shipyard.Prototypes; using Robust.Client.AutoGenerated; using Robust.Client.UserInterface.Controls; using Robust.Client.UserInterface.XAML; using Robust.Shared.Prototypes; using static Robust.Client.UserInterface.Controls.BaseButton; using Robust.Client.Player; namespace Content.Client._NF.Shipyard.UI; [GenerateTypedNameReferences] public sealed partial class ShipyardConsoleMenu : FancyWindow { [Dependency] private readonly IPrototypeManager _protoManager = default!; [Dependency] private readonly IEntityManager _entMan = default!; // Horizon [Dependency] private readonly IPlayerManager _player = default!; // Horizon public event Action? OnSellShip; public event Action? OnOrderApproved; private readonly List _categoryStrings = new(); private readonly List _classStrings = new(); private readonly List _engineStrings = new(); private VesselSize? _category; private VesselClass? _class; private VesselEngine? _engine; private List _lastAvailableProtos = new(); private List _lastUnavailableProtos = new(); private bool _freeListings = false; private bool _validId = false; private ConfirmButton? _currentlyConfirmingButton = null; public EntityUid Owner = EntityUid.Invalid; // Horizon public ShipyardConsoleMenu() { RobustXamlLoader.Load(this); IoCManager.InjectDependencies(this); Title = Loc.GetString("shipyard-console-menu-title"); SearchBar.OnTextChanged += OnSearchBarTextChanged; Categories.OnItemSelected += OnCategoryItemSelected; Classes.OnItemSelected += OnClassItemSelected; Engines.OnItemSelected += OnEngineItemSelected; SellShipButton.OnPressed += (args) => { OnSellShip?.Invoke(args); }; } private void OnCategoryItemSelected(OptionButton.ItemSelectedEventArgs args) { SetCategoryText(args.Id); PopulateProducts(_lastAvailableProtos, _lastUnavailableProtos, _freeListings, _validId); } private void OnClassItemSelected(OptionButton.ItemSelectedEventArgs args) { SetClassText(args.Id); PopulateProducts(_lastAvailableProtos, _lastUnavailableProtos, _freeListings, _validId); } private void OnEngineItemSelected(OptionButton.ItemSelectedEventArgs args) { SetEngineText(args.Id); PopulateProducts(_lastAvailableProtos, _lastUnavailableProtos, _freeListings, _validId); } private void OnSearchBarTextChanged(LineEdit.LineEditEventArgs args) { PopulateProducts(_lastAvailableProtos, _lastUnavailableProtos, _freeListings, _validId); } private void SetCategoryText(int id) { _category = id == 0 ? null : _categoryStrings[id]; Categories.SelectId(id); } private void SetClassText(int id) { _class = id == 0 ? null : _classStrings[id]; Classes.SelectId(id); } private void SetEngineText(int id) { _engine = id == 0 ? null : _engineStrings[id]; Engines.SelectId(id); } /// /// Populates the list of products that will actually be shown, using the current filters. /// public void PopulateProducts(List availablePrototypes, List unavailablePrototypes, bool free, bool canPurchase) { Vessels.RemoveAllChildren(); var search = SearchBar.Text.Trim().ToLowerInvariant(); var newVessels = GetVesselPrototypesFromIds(availablePrototypes); AddVesselsToControls(newVessels, search, free, canPurchase); var newUnavailableVessels = GetVesselPrototypesFromIds(unavailablePrototypes); AddVesselsToControls(newUnavailableVessels, search, free, false); _lastAvailableProtos = availablePrototypes; _lastUnavailableProtos = unavailablePrototypes; } /// /// Given a set of prototype IDs, returns a corresponding set of prototypes, ordered by name. /// private List GetVesselPrototypesFromIds(IEnumerable protoIds) { var vesselList = protoIds.Select(it => _protoManager.TryIndex(it, out var proto) ? proto : null) .Where(it => it != null) .ToList(); vesselList.Sort((x, y) => string.Compare(x!.Name, y!.Name, StringComparison.CurrentCultureIgnoreCase)); return vesselList; } /// /// Adds all vessels in a given list of prototypes as VesselRows in the UI. /// private void AddVesselsToControls(IEnumerable vessels, string search, bool free, bool canPurchase) { foreach (var prototype in vessels) { // Filter any ships if (_category != null && !prototype!.Category.Equals(_category)) continue; if (_class != null && !prototype!.Classes.Contains(_class.Value)) continue; if (_engine != null && !prototype!.Engines.Contains(_engine.Value)) continue; if (search.Length > 0 && !prototype!.Name.ToLowerInvariant().Contains(search)) continue; string priceText; if (free) priceText = Loc.GetString("shipyard-console-menu-listing-free"); else { // Horizon tweak start var cost = prototype!.Price; foreach (var item in prototype!.CostModifiers) item.Modify(_player.LocalEntity, Owner, ref cost, _entMan); priceText = BankSystemExtensions.ToSpesoString(cost); var diff = cost - prototype.Price; var percentage = (float)diff / (float)prototype.Price * 100.0f; if (diff != 0) priceText += $" ({(diff > 0 ? "+" : "")}{percentage:F1}%)"; // Horizon tweak end } var vesselEntry = new VesselRow { Vessel = prototype, VesselName = { Text = prototype!.Name }, Purchase = { Text = Loc.GetString("shipyard-console-purchase-available"), Disabled = !canPurchase }, Guidebook = { Disabled = prototype.GuidebookPage is null, TooltipDelay = 0.2f, ToolTip = prototype.Description }, Price = { Text = priceText }, }; vesselEntry.Purchase.OnConfirming += OnStartConfirmingPurchase; vesselEntry.Purchase.OnPressed += (args) => { _currentlyConfirmingButton = null; OnOrderApproved?.Invoke(args); }; Vessels.AddChild(vesselEntry); } } /// /// Confirming handler: ensures that only one button is confirming at a time. /// private void OnStartConfirmingPurchase(ButtonEventArgs args) { if (args.Button is not ConfirmButton confirmButton) return; if (_currentlyConfirmingButton != null) _currentlyConfirmingButton.ClearIsConfirming(); _currentlyConfirmingButton = confirmButton; } /// /// Populates the list categories that will actually be shown, using the current filters. /// public void PopulateCategories(List availablePrototypes, List unavailablePrototypes) { _categoryStrings.Clear(); Categories.Clear(); AddCategoriesFromPrototypes(availablePrototypes); AddCategoriesFromPrototypes(unavailablePrototypes); _categoryStrings.Sort(); // Add "All" category at the top of the list _categoryStrings.Insert(0, VesselSize.All); foreach (var str in _categoryStrings) { Categories.AddItem(Loc.GetString($"shipyard-console-category-{str}")); } } /// /// Adds all ship categories from a list of vessel prototypes to the current control's list if they are missing. /// private void AddCategoriesFromPrototypes(IEnumerable prototypes) { foreach (var protoId in prototypes) { if (!_protoManager.TryIndex(protoId, out var prototype)) continue; if (!_categoryStrings.Contains(prototype.Category) && prototype.Category != VesselSize.All) { _categoryStrings.Add(prototype.Category); } } } /// /// Populates the list classes that will actually be shown, using the current filters. /// public void PopulateClasses(List availablePrototypes, List unavailablePrototypes) { _classStrings.Clear(); Classes.Clear(); AddClassesFromPrototypes(availablePrototypes); AddClassesFromPrototypes(unavailablePrototypes); _classStrings.Sort(); // Add "All" category at the top of the list _classStrings.Insert(0, VesselClass.All); foreach (var str in _classStrings) { Classes.AddItem(Loc.GetString($"shipyard-console-class-{str}")); } } /// /// Adds all ship classes from a list of vessel prototypes to the current control's list if they are missing. /// private void AddClassesFromPrototypes(IEnumerable prototypes) { foreach (var protoId in prototypes) { if (!_protoManager.TryIndex(protoId, out var prototype)) continue; foreach (var cl in prototype.Classes) { if (!_classStrings.Contains(cl) && cl != VesselClass.All) { _classStrings.Add(cl); } } } } /// /// Populates the list engines that will actually be shown, using the current filters. /// public void PopulateEngines(List availablePrototypes, List unavailablePrototypes) { _engineStrings.Clear(); Engines.Clear(); AddEnginesFromPrototypes(availablePrototypes); AddEnginesFromPrototypes(unavailablePrototypes); _engineStrings.Sort(); // Add "All" category at the top of the list _engineStrings.Insert(0, VesselEngine.All); foreach (var str in _engineStrings) { Engines.AddItem(Loc.GetString($"shipyard-console-engine-{str}")); } } /// /// Adds all ship engine power type from a list of vessel prototypes to the current control's list if they are missing. /// private void AddEnginesFromPrototypes(IEnumerable prototypes) { foreach (var protoId in prototypes) { if (!_protoManager.TryIndex(protoId, out var prototype)) continue; foreach (var cl in prototype.Engines) { if (!_engineStrings.Contains(cl) && cl != VesselEngine.All) { _engineStrings.Add(cl); } } } } public void UpdateState(ShipyardConsoleInterfaceState state) { BalanceLabel.Text = BankSystemExtensions.ToSpesoString(state.Balance); var shipPrice = 0; if (!state.FreeListings) shipPrice = state.ShipSellValue; ShipAppraisalLabel.Text = $"{BankSystemExtensions.ToSpesoString(shipPrice)} ({state.SellRate * 100.0f:F1}%)"; SellShipButton.Disabled = state.ShipDeedTitle == null; TargetIdButton.Text = state.IsTargetIdPresent ? Loc.GetString("id-card-console-window-eject-button") : Loc.GetString("id-card-console-window-insert-button"); if (state.ShipDeedTitle != null) { DeedTitle.Text = state.ShipDeedTitle; } else { DeedTitle.Text = $"None"; } _freeListings = state.FreeListings; _validId = state.IsTargetIdPresent; PopulateProducts(_lastAvailableProtos, _lastUnavailableProtos, _freeListings, _validId); } }