Blog/Components/Pages/Warframe.razor.cs 4.3 K · 123 lines · raw · history

1 using System.Globalization;
2 using Blog.Models;
3 using Blog.Services;
4 using Microsoft.AspNetCore.Components;
5
6 namespace Blog.Components.Pages;
7
8 /// <summary>
9 /// Searches Digital Extremes' published drop tables and answers the question they don't: given a
10 /// part, where is the best place to go and get it.
11 /// </summary>
12 public partial class Warframe : ComponentBase
13 {
14 private const int MaxSources = 20;
15
16 private const int MaxRelicSources = 3;
17
18 private const int MaxMatches = 40;
19
20 [Inject]
21 public required WarframeDropService Service { get; set; }
22
23 [SupplyParameterFromQuery(Name = "q")]
24 public string? Query { get; set; }
25
26 [SupplyParameterFromQuery(Name = "item")]
27 public string? ItemName { get; set; }
28
29 private DropTableStatus Status { get; set; } = new(null, null);
30
31 private DropTables? Tables => Status.Tables;
32
33 private IReadOnlyList<string> Matches { get; set; } = [];
34
35 /// <summary>The item the page is about, if the search settled on one.</summary>
36 private ItemDrops? Item { get; set; }
37
38 protected override async Task OnParametersSetAsync()
39 {
40 Status = await Service.GetTablesAsync().ConfigureAwait(true);
41
42 Matches = [];
43 Item = null;
44
45 if (Tables is not { } tables) return;
46
47 if (!string.IsNullOrWhiteSpace(Query))
48 {
49 Matches = tables.Search(Query, MaxMatches);
50 }
51
52 // An explicit ?item= wins; a search with one hit needs no second click.
53 Item = tables.Find(ItemName)
54 ?? (Matches.Count == 1 ? tables.Find(Matches[0]) : null);
55
56 await base.OnParametersSetAsync().ConfigureAwait(true);
57 }
58
59 /// <summary>
60 /// The relics holding this item, each with the best places to farm the relic itself. Relics
61 /// that still drop come first, then the better chance within each group.
62 /// </summary>
63 private IReadOnlyList<(RelicLine Line, IReadOnlyList<DropSource> Sources)> RelicRoutes(ItemDrops item) =>
64 item.InRelics
65 .Select(line => (
66 Line: line,
67 Sources: (IReadOnlyList<DropSource>)Tables!.SourcesFor(line.Relic).Take(MaxRelicSources).ToArray()))
68 .OrderByDescending(route => route.Sources.Count > 0)
69 .ThenByDescending(route => route.Line.Intact)
70 .ToArray();
71
72 private string SearchLink(string name) =>
73 $"/Warframe?q={Uri.EscapeDataString(Query ?? name)}&item={Uri.EscapeDataString(name)}";
74
75 private string ItemLink(string name) => $"/Warframe?item={Uri.EscapeDataString(name)}";
76
77 /// <summary>
78 /// A chance without the trailing zeroes. Enemy chances are products of two percentages and can
79 /// land under a tenth of a percent, so small numbers keep more digits.
80 /// </summary>
81 private static string Percent(double value) => value switch
82 {
83 0 => "",
84 < 0.01 => "<0.01%",
85 < 1 => value.ToString("0.###", CultureInfo.InvariantCulture) + "%",
86 _ => value.ToString("0.##", CultureInfo.InvariantCulture) + "%"
87 };
88
89 private static string CategoryLabel(DropCategory category) => category switch
90 {
91 DropCategory.Mission => "Mission",
92 DropCategory.Key => "Quest / key",
93 DropCategory.Dynamic => "Activity",
94 DropCategory.Sortie => "Sortie",
95 DropCategory.Bounty => "Bounty",
96 DropCategory.Enemy => "Enemy",
97 _ => category.ToString()
98 };
99
100 /// <summary>
101 /// The size of the index. Invariant rather than the request's culture: the sentence around it
102 /// is English, and "3.481 items" reads as a different number.
103 /// </summary>
104 private static string ItemCount(DropTables tables) =>
105 tables.Names.Count.ToString("N0", CultureInfo.InvariantCulture);
106
107 /// <summary>
108 /// How long ago this copy of the page was downloaded, taken from the file's own timestamp. Days
109 /// is the unit that matters here: the copy changes only when the site is deployed.
110 /// </summary>
111 private static string Age(DateTimeOffset fetchedAt)
112 {
113 var age = DateTimeOffset.UtcNow - fetchedAt;
114 return age switch
115 {
116 { TotalMinutes: < 1 } => "just now",
117 { TotalHours: < 1 } => $"{(int)age.TotalMinutes} min ago",
118 { TotalDays: < 1 } => $"{(int)age.TotalHours} h ago",
119 { TotalDays: < 2 } => "yesterday",
120 _ => $"{(int)age.TotalDays} days ago"
121 };
122 }
123 }