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 long ago this copy of the page was downloaded, taken from the file's own timestamp. Days
/// is the unit that matters here: the copy changes only when the site is deployed.
///
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",
{ TotalDays: < 2 } => "yesterday",
_ => $"{(int)age.TotalDays} days ago"
};
}
}