using System.Globalization; using Blog.Models; using Blog.Services; using Microsoft.AspNetCore.Components; namespace Blog.Components.Pages; /// /// Searches Digital Extremes' published drop tables and answers the question they don't: given a /// part, where is the best place to go and get it. /// public partial class Warframe : ComponentBase { private const int MaxSources = 20; private const int MaxRelicSources = 3; private const int MaxMatches = 40; [Inject] public required WarframeDropService Service { get; set; } [SupplyParameterFromQuery(Name = "q")] public string? Query { get; set; } [SupplyParameterFromQuery(Name = "item")] public string? ItemName { get; set; } private DropTableStatus Status { get; set; } = new(null, null); private DropTables? Tables => Status.Tables; private IReadOnlyList Matches { get; set; } = []; /// The item the page is about, if the search settled on one. private ItemDrops? Item { get; set; } protected override async Task OnParametersSetAsync() { Status = await Service.GetTablesAsync().ConfigureAwait(true); Matches = []; Item = null; if (Tables is not { } tables) return; if (!string.IsNullOrWhiteSpace(Query)) { Matches = tables.Search(Query, MaxMatches); } // An explicit ?item= wins; a search with one hit needs no second click. Item = tables.Find(ItemName) ?? (Matches.Count == 1 ? tables.Find(Matches[0]) : null); await base.OnParametersSetAsync().ConfigureAwait(true); } /// /// The relics holding this item, each with the best places to farm the relic itself. Relics /// that still drop come first, then the better chance within each group. /// private IReadOnlyList<(RelicLine Line, IReadOnlyList Sources)> RelicRoutes(ItemDrops item) => item.InRelics .Select(line => ( Line: line, Sources: (IReadOnlyList)Tables!.SourcesFor(line.Relic).Take(MaxRelicSources).ToArray())) .OrderByDescending(route => route.Sources.Count > 0) .ThenByDescending(route => route.Line.Intact) .ToArray(); private string SearchLink(string name) => $"/Warframe?q={Uri.EscapeDataString(Query ?? name)}&item={Uri.EscapeDataString(name)}"; private string ItemLink(string name) => $"/Warframe?item={Uri.EscapeDataString(name)}"; /// /// A chance without the trailing zeroes. Enemy chances are products of two percentages and can /// land under a tenth of a percent, so small numbers keep more digits. /// private static string Percent(double value) => value switch { 0 => "", < 0.01 => "<0.01%", < 1 => value.ToString("0.###", CultureInfo.InvariantCulture) + "%", _ => value.ToString("0.##", CultureInfo.InvariantCulture) + "%" }; private static string CategoryLabel(DropCategory category) => category switch { DropCategory.Mission => "Mission", DropCategory.Key => "Quest / key", DropCategory.Dynamic => "Activity", DropCategory.Sortie => "Sortie", DropCategory.Bounty => "Bounty", DropCategory.Enemy => "Enemy", _ => category.ToString() }; /// /// The size of the index. Invariant rather than the request's culture: the sentence around it /// is English, and "3.481 items" reads as a different number. /// private static string ItemCount(DropTables tables) => tables.Names.Count.ToString("N0", CultureInfo.InvariantCulture); /// How stale the copy on screen is. private static string Age(DateTimeOffset fetchedAt) { var age = DateTimeOffset.UtcNow - fetchedAt; return age switch { { TotalMinutes: < 1 } => "just now", { TotalHours: < 1 } => $"{(int)age.TotalMinutes} min ago", { TotalDays: < 1 } => $"{(int)age.TotalHours} h ago", _ => $"{(int)age.TotalDays} d ago" }; } }