332 lines
13 KiB
Plaintext
332 lines
13 KiB
Plaintext
@page "/docs"
|
|
@using System.Text.RegularExpressions
|
|
|
|
<PageTitle>Fellowship Docs</PageTitle>
|
|
|
|
<div class="docs-page">
|
|
<div class="docs-header">
|
|
<h1 class="docs-title">Fellowship Reference</h1>
|
|
<p class="docs-subtitle">@DocsData.All.Count entries across @Groups.Count() categories</p>
|
|
<div class="docs-search">
|
|
<span class="search-icon">🔍</span>
|
|
<input type="text" class="search-input" placeholder="Search docs..." @bind="searchTerm" @bind:after="OnSearchChanged" />
|
|
@if (!string.IsNullOrEmpty(searchTerm))
|
|
{
|
|
<button class="search-clear" @onclick="ClearSearch">×</button>
|
|
}
|
|
</div>
|
|
<div class="docs-filter-bar">
|
|
<button class="filter-btn @(activeFilter == null ? "active" : "")" @onclick="FilterAll">All</button>
|
|
<button class="filter-btn @(activeFilter == "Skill" ? "active" : "")" @onclick="FilterSkills">Skills</button>
|
|
<button class="filter-btn @(activeFilter == "Debuff" ? "active" : "")" @onclick="FilterDebuffs">Debuffs</button>
|
|
<button class="filter-btn @(activeFilter == "Buff" ? "active" : "")" @onclick="FilterBuffs">Buffs</button>
|
|
<button class="filter-btn @(activeFilter == "Key" ? "active" : "")" @onclick="FilterKeys">Keys</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="docs-body">
|
|
<nav class="docs-sidebar">
|
|
<div class="sidebar-inner">
|
|
@foreach (var group in FilteredGroups)
|
|
{
|
|
var groupId = GetGroupId(group.Key);
|
|
<div class="sidebar-group">
|
|
<a class="sidebar-group-title" href="@($"#{groupId}")">@group.Key</a>
|
|
<div class="sidebar-items">
|
|
@foreach (var doc in group)
|
|
{
|
|
var docId = GetDocId(doc);
|
|
<a class="sidebar-item" href="@($"#{docId}")" title="@doc.FileName">
|
|
<span class="sidebar-item-badge @GetTypeClass(doc)"></span>
|
|
@GetDisplayName(doc)
|
|
</a>
|
|
}
|
|
</div>
|
|
</div>
|
|
}
|
|
</div>
|
|
</nav>
|
|
|
|
<div class="docs-content">
|
|
@if (!FilteredGroups.Any())
|
|
{
|
|
<div class="no-results">
|
|
<p>No docs match "@searchTerm"</p>
|
|
<button class="filter-btn" @onclick="ClearSearch">Clear search</button>
|
|
</div>
|
|
}
|
|
|
|
@foreach (var group in FilteredGroups)
|
|
{
|
|
var groupId = GetGroupId(group.Key);
|
|
<section class="group-section">
|
|
<div class="group-header" id="@groupId">
|
|
<h2 class="group-title">@group.Key</h2>
|
|
<span class="group-count">@group.Count()</span>
|
|
</div>
|
|
|
|
@foreach (var doc in group)
|
|
{
|
|
var docId = GetDocId(doc);
|
|
var typeName = doc.GetType().Name.Replace("Doc", "");
|
|
<article class="doc-card" id="@docId">
|
|
<div class="doc-card-header">
|
|
<div class="doc-card-title-row">
|
|
<h3 class="doc-card-title">@GetDisplayName(doc)</h3>
|
|
<span class="doc-type-badge @GetTypeClass(doc)">@typeName</span>
|
|
</div>
|
|
@if (doc is SkillDoc { Key: { } key } && !string.IsNullOrEmpty(key))
|
|
{
|
|
<div class="doc-key-badge">
|
|
<span class="key-icon">⌨</span>
|
|
<span class="key-text">@key</span>
|
|
</div>
|
|
}
|
|
</div>
|
|
|
|
<div class="doc-card-body">
|
|
@if (doc is SkillDoc { Description: { } desc } && !string.IsNullOrEmpty(desc))
|
|
{
|
|
<div class="doc-description">
|
|
@{
|
|
var lines = desc.Split('\n');
|
|
foreach (var line in lines)
|
|
{
|
|
<p>@RenderWikiLinks(line)</p>
|
|
}
|
|
}
|
|
</div>
|
|
}
|
|
|
|
<div class="doc-fields">
|
|
@foreach (var field in GetOrderedFields(doc))
|
|
{
|
|
<div class="doc-field">
|
|
<span class="doc-field-label">@field.Label</span>
|
|
<span class="doc-field-value">@field.Value</span>
|
|
</div>
|
|
}
|
|
</div>
|
|
|
|
@if (GetBody(doc) is { } body && !string.IsNullOrWhiteSpace(body) && (doc is not SkillDoc || string.IsNullOrWhiteSpace(((SkillDoc)doc).Description)))
|
|
{
|
|
<div class="doc-body">
|
|
<pre class="doc-body-text">@body.Trim()</pre>
|
|
</div>
|
|
}
|
|
</div>
|
|
|
|
@if (doc is SkillDoc { Tags: { } tags } && tags.Count > 0)
|
|
{
|
|
<div class="doc-card-footer">
|
|
@foreach (var tag in tags)
|
|
{
|
|
<span class="tag-badge">@tag</span>
|
|
}
|
|
</div>
|
|
}
|
|
</article>
|
|
}
|
|
</section>
|
|
}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
@code {
|
|
private string? searchTerm;
|
|
private string? activeFilter;
|
|
private List<IGrouping<string, DocEntry>> Groups = [];
|
|
private IEnumerable<IGrouping<string, DocEntry>> FilteredGroups => Groups
|
|
.Where(g => g.Any(d => MatchesFilter(d)))
|
|
.OrderBy(g => SortOrder(g.Key));
|
|
|
|
protected override void OnInitialized()
|
|
{
|
|
Groups = DocsData.All
|
|
.GroupBy(d => GetCharacterOrType(d))
|
|
.ToList();
|
|
}
|
|
|
|
private bool MatchesFilter(DocEntry doc)
|
|
{
|
|
if (!string.IsNullOrEmpty(searchTerm))
|
|
{
|
|
var term = searchTerm.Trim().ToLowerInvariant();
|
|
var name = GetDisplayName(doc).ToLowerInvariant();
|
|
if (!name.Contains(term)) return false;
|
|
}
|
|
if (activeFilter != null)
|
|
{
|
|
var type = doc.GetType().Name.Replace("Doc", "");
|
|
if (type != activeFilter) return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
private static int SortOrder(string groupKey) => groupKey switch
|
|
{
|
|
"Xavian" => 0,
|
|
"Rime" => 1,
|
|
"Vigour" => 2,
|
|
"Keys" => 3,
|
|
_ => 99
|
|
};
|
|
|
|
private void OnSearchChanged()
|
|
{
|
|
StateHasChanged();
|
|
}
|
|
|
|
private void ClearSearch()
|
|
{
|
|
searchTerm = null;
|
|
activeFilter = null;
|
|
}
|
|
|
|
private void FilterAll() { activeFilter = null; }
|
|
private void FilterSkills() { activeFilter = activeFilter == "Skill" ? null : "Skill"; }
|
|
private void FilterDebuffs() { activeFilter = activeFilter == "Debuff" ? null : "Debuff"; }
|
|
private void FilterBuffs() { activeFilter = activeFilter == "Buff" ? null : "Buff"; }
|
|
private void FilterKeys() { activeFilter = activeFilter == "Key" ? null : "Key"; }
|
|
|
|
private static string GetCharacterOrType(DocEntry doc) => doc switch
|
|
{
|
|
SkillDoc s => s.Character,
|
|
DebuffDoc d => d.Character,
|
|
BuffDoc b => b.Character,
|
|
CharacterDoc c => c.Character,
|
|
KeyDoc => "Keys",
|
|
_ => "Other"
|
|
};
|
|
|
|
private static string GetDisplayName(DocEntry doc) =>
|
|
Path.GetFileNameWithoutExtension(doc.FileName);
|
|
|
|
private static string GetGroupId(string group) =>
|
|
$"group-{group.GetHashCode():x}";
|
|
|
|
private static string GetDocId(DocEntry doc) =>
|
|
$"doc-{doc.FileName.GetHashCode():x}";
|
|
|
|
private static string GetTypeClass(DocEntry doc) => doc switch
|
|
{
|
|
SkillDoc => "type-skill",
|
|
DebuffDoc => "type-debuff",
|
|
BuffDoc => "type-buff",
|
|
KeyDoc => "type-key",
|
|
CharacterDoc => "type-character",
|
|
_ => ""
|
|
};
|
|
|
|
private static MarkupString RenderWikiLinks(string line)
|
|
{
|
|
var result = Regex.Replace(line, @"\[\[([^\]]+)\]\]", m =>
|
|
{
|
|
var name = m.Groups[1].Value;
|
|
return $"<span class=\"wiki-link\">{name}</span>";
|
|
});
|
|
return new MarkupString(result);
|
|
}
|
|
|
|
private static readonly Dictionary<string, string> FieldLabels = new()
|
|
{
|
|
["Character"] = "Character",
|
|
["Cast"] = "Cast Time",
|
|
["Key"] = "Key Bind",
|
|
["Range"] = "Range",
|
|
["Damage"] = "Damage",
|
|
["DamageType"] = "Damage Type",
|
|
["Heal"] = "Healing",
|
|
["Shield"] = "Shield",
|
|
["Cooldown"] = "Cooldown",
|
|
["Mana"] = "Mana Cost",
|
|
["OffGlobalCooldown"] = "Off GCD",
|
|
["Gdc"] = "GDC",
|
|
["Duration"] = "Duration",
|
|
["DamageReduction"] = "Dmg Reduction",
|
|
["DamageTickTime"] = "Tick Interval",
|
|
["IsToggle"] = "Toggle",
|
|
["ManaUpkeepTick"] = "Mana Upkeep",
|
|
["ParryChance"] = "Parry Chance",
|
|
["DamageRedirection"] = "Dmg Redirection",
|
|
["SpiritCost"] = "Spirit Cost",
|
|
["SecondEffectDuration"] = "Effect 2 Duration",
|
|
["SecondEffectDamageReduction"] = "Effect 2 Dmg Reduction",
|
|
["HealingDuration"] = "Healing Duration",
|
|
["HealingTickTime"] = "Healing Tick",
|
|
["CostSwiftReprieval"] = "Cost: Swift Reprieval",
|
|
["GdcDuration"] = "GDC Duration",
|
|
["Effect"] = "Effect",
|
|
["GeneratesSpirit"] = "Generates Spirit",
|
|
["AreaDamagePercentage"] = "Cleave %",
|
|
["SwiftReprievalChance"] = "Swift Reprieval %",
|
|
["MaxStacks"] = "Max Stacks",
|
|
["Action"] = "Action",
|
|
["ParryChanceBonus"] = "Parry Bonus",
|
|
["ManaRestoreBase"] = "Mana Restore Base",
|
|
["ManaRestorePerStack"] = "Mana Per Stack",
|
|
["Order"] = "Order",
|
|
["Priority"] = "Priority",
|
|
["Completed"] = "Completed",
|
|
["Raw"] = "Raw",
|
|
};
|
|
|
|
private static readonly Dictionary<Type, string[]> FieldOrder = new()
|
|
{
|
|
[typeof(SkillDoc)] = ["Character", "Cast", "Key", "Range", "Damage", "DamageType", "Heal", "Shield", "Cooldown", "Mana", "OffGlobalCooldown", "Gdc", "Duration", "DamageReduction", "DamageTickTime", "IsToggle", "ManaUpkeepTick", "ParryChance", "DamageRedirection", "GeneratesSpirit", "AreaDamagePercentage", "SwiftReprievalChance", "Effect", "SpiritCost", "SecondEffectDuration", "SecondEffectDamageReduction", "HealingDuration", "HealingTickTime", "CostSwiftReprieval", "GdcDuration"],
|
|
[typeof(DebuffDoc)] = ["Character", "MaxStacks", "Duration", "ParryChanceBonus", "ManaRestoreBase", "ManaRestorePerStack"],
|
|
[typeof(BuffDoc)] = ["Character", "MaxStacks"],
|
|
[typeof(KeyDoc)] = ["Action"],
|
|
[typeof(CharacterDoc)] = ["Character"],
|
|
};
|
|
|
|
private static List<FieldEntry> GetOrderedFields(DocEntry doc)
|
|
{
|
|
var result = new List<FieldEntry>();
|
|
var type = doc.GetType();
|
|
var order = FieldOrder.GetValueOrDefault(type, []);
|
|
var allProps = new Dictionary<string, string>();
|
|
|
|
foreach (var prop in type.GetProperties(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance))
|
|
{
|
|
var name = prop.Name;
|
|
if (name is "FileName" or "FilePath" or "Body" or "Description") continue;
|
|
|
|
var value = prop.GetValue(doc);
|
|
if (value == null) continue;
|
|
|
|
if (value is bool bVal && !bVal) continue;
|
|
if (value is string s && string.IsNullOrEmpty(s)) continue;
|
|
|
|
var display = value switch
|
|
{
|
|
List<string> list => string.Join(", ", list),
|
|
_ => value.ToString()
|
|
};
|
|
|
|
if (string.IsNullOrEmpty(display)) continue;
|
|
allProps[name] = display;
|
|
}
|
|
|
|
foreach (var key in order)
|
|
{
|
|
if (allProps.Remove(key, out var val))
|
|
{
|
|
result.Add(new FieldEntry(FieldLabels.GetValueOrDefault(key, key), val));
|
|
}
|
|
}
|
|
|
|
foreach (var (key, val) in allProps)
|
|
{
|
|
result.Add(new FieldEntry(FieldLabels.GetValueOrDefault(key, key), val));
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
private static string? GetBody(DocEntry doc) => doc.Body;
|
|
|
|
private record FieldEntry(string Label, string Value);
|
|
}
|