Adding quick vibe reference

This commit is contained in:
2026-10-23 11:18:37 -04:00
parent c78eeca0f6
commit 0a72031265
30 changed files with 1536 additions and 15 deletions
@@ -20,7 +20,7 @@ public static class FactionPowerItems
LimitedTo: "Vynthra", LimitedTo: "Vynthra",
LimitedToMerc: null, LimitedToMerc: null,
MinimumLevel: 10, MinimumLevel: 10,
ImageFileName: null, ImageFileName: "Destroyer.png",
Body: ""); Body: "");
public static readonly FactionPowerItemData ForcedAssimilation = new( public static readonly FactionPowerItemData ForcedAssimilation = new(
@@ -50,7 +50,7 @@ public static class FactionPowerItems
LimitedTo: "Vynthra", LimitedTo: "Vynthra",
LimitedToMerc: null, LimitedToMerc: null,
MinimumLevel: 1, MinimumLevel: 1,
ImageFileName: null, ImageFileName: "Healing Pod.png",
Body: ""); Body: "");
public static readonly FactionPowerItemData Infusion = new( public static readonly FactionPowerItemData Infusion = new(
@@ -60,7 +60,7 @@ public static class FactionPowerItems
LimitedTo: "Vynthra", LimitedTo: "Vynthra",
LimitedToMerc: null, LimitedToMerc: null,
MinimumLevel: 1, MinimumLevel: 1,
ImageFileName: null, ImageFileName: "Infusion.png",
Body: ""); Body: "");
public static readonly FactionPowerItemData Minefield = new( public static readonly FactionPowerItemData Minefield = new(
@@ -90,7 +90,7 @@ public static class FactionPowerItems
LimitedTo: null, LimitedTo: null,
LimitedToMerc: "The Molten", LimitedToMerc: "The Molten",
MinimumLevel: 3, MinimumLevel: 3,
ImageFileName: null, ImageFileName: "Orbital Laser.png",
Body: ""); Body: "");
public static readonly FactionPowerItemData Reinforcements = new( public static readonly FactionPowerItemData Reinforcements = new(
@@ -170,7 +170,7 @@ public static class FactionPowerItems
LimitedTo: "Vynthra", LimitedTo: "Vynthra",
LimitedToMerc: null, LimitedToMerc: null,
MinimumLevel: 3, MinimumLevel: 3,
ImageFileName: null, ImageFileName: "Tree of Life.png",
Body: ""); Body: "");
public static IReadOnlyList<FactionPowerItemData> All { get; } = public static IReadOnlyList<FactionPowerItemData> All { get; } =
@@ -7,7 +7,7 @@ public static class PlanetaryMasteryItems
Id: "Accelerant", Id: "Accelerant",
Name: "Accelerant", Name: "Accelerant",
Levels: ["10% cooldown reduction on Topbar abilities.", "15% cooldown reduction on Topbar abilities.", "20% cooldown reduction on Topbar abilities.", "25% cooldown reduction on Topbar abilities.", "30% cooldown reduction on Topbar abilities."], Levels: ["10% cooldown reduction on Topbar abilities.", "15% cooldown reduction on Topbar abilities.", "20% cooldown reduction on Topbar abilities.", "25% cooldown reduction on Topbar abilities.", "30% cooldown reduction on Topbar abilities."],
ImageFileName: null, ImageFileName: "Accelerant.png",
Body: "Grass gives [[Vynthra]] reduce damage taken. So I want cooldown reduction on Topbar to cast as many Healing Pods as possible.\n\nGiven Healing Pods also spawns three Seedlings, it helps with [[Vynthra]]'s weakness of getting seedlings where needed, and I want those power AOE turrets that poison and slow that [[Vynthra]] has everywhere to spread the grass and deal with the constant random raids."); Body: "Grass gives [[Vynthra]] reduce damage taken. So I want cooldown reduction on Topbar to cast as many Healing Pods as possible.\n\nGiven Healing Pods also spawns three Seedlings, it helps with [[Vynthra]]'s weakness of getting seedlings where needed, and I want those power AOE turrets that poison and slow that [[Vynthra]] has everywhere to spread the grass and deal with the constant random raids.");
public static readonly PlanetaryMasteryItemData Battery = new( public static readonly PlanetaryMasteryItemData Battery = new(
@@ -77,6 +77,10 @@
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 16 16'%3E%3Crect x='2' y='2' width='12' height='12' rx='2.5' fill='none' stroke='%2334d399' stroke-width='1.3'/%3E%3Ccircle cx='8' cy='8' r='2' fill='%2334d399'/%3E%3C/svg%3E"); background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 16 16'%3E%3Crect x='2' y='2' width='12' height='12' rx='2.5' fill='none' stroke='%2334d399' stroke-width='1.3'/%3E%3Ccircle cx='8' cy='8' r='2' fill='%2334d399'/%3E%3C/svg%3E");
} }
.bi-reference-nav-menu {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='%2338bdf8' viewBox='0 0 16 16'%3E%3Cpath d='M1 2.828c.885-.37 2.154-.769 3.388-.893 1.33-.134 2.458.063 3.112.752v9.746c-.935-.53-2.12-.603-3.213-.493-1.18.12-2.37.461-3.287.811V2.828zm7.5-.141c.654-.689 1.782-.886 3.112-.752 1.234.124 2.503.523 3.388.893v10.749c-.917-.35-2.107-.691-3.287-.81-1.094-.111-2.278-.039-3.213.492V2.687zM8 1.783C7.015.936 5.587.81 4.287.94c-1.514.153-3.042.672-3.994 1.105A.5.5 0 0 0 0 2.5v11a.5.5 0 0 0 .707.455c.882-.4 2.303-.881 3.68-1.02 1.409-.142 2.59.087 3.223.877a.5.5 0 0 0 .78 0c.633-.79 1.814-1.019 3.222-.877 1.378.139 2.8.62 3.681 1.02A.5.5 0 0 0 16 13.5v-11a.5.5 0 0 0-.293-.455c-.952-.433-2.48-.952-3.994-1.105C10.413.809 8.985.936 8 1.783z'/%3E%3C/svg%3E");
}
.nav-item { .nav-item {
font-size: 0.92rem; font-size: 0.92rem;
padding-bottom: 0.4rem; padding-bottom: 0.4rem;
+30 -8
View File
@@ -2,7 +2,7 @@
<PageTitle>Loadout</PageTitle> <PageTitle>Loadout</PageTitle>
<div class="section-header d-flex align-items-center mb-4"> <div class="section-header d-flex align-items-center mb-4 flex-wrap gap-2">
<h1 class="mb-0">Loadout</h1> <h1 class="mb-0">Loadout</h1>
<div class="ms-4 d-flex align-items-center gap-2"> <div class="ms-4 d-flex align-items-center gap-2">
<span class="hero-label">Hero:</span> <span class="hero-label">Hero:</span>
@@ -16,6 +16,9 @@
} }
</div> </div>
<div class="ms-3 flex-grow-1 border-bottom opacity-25"></div> <div class="ms-3 flex-grow-1 border-bottom opacity-25"></div>
<a href="/reference" class="btn btn-sm btn-scifi-outline ms-2">
<span>Item Reference Docs ↗</span>
</a>
</div> </div>
<div class="loadout-layout"> <div class="loadout-layout">
@@ -45,7 +48,12 @@
@if (_showDetails) @if (_showDetails)
{ {
<div class="pool-body"> <div class="pool-body">
<div class="pool-name">@item.Name</div> <div class="d-flex align-items-center justify-content-between gap-1">
<div class="pool-name">@item.Name</div>
<a href="@item.DocUrl" class="pool-doc-link" title="View @item.Name reference doc" @onclick:stopPropagation draggable="false">
<span>Doc ↗</span>
</a>
</div>
@if (item.LimitedToMerc is not null) @if (item.LimitedToMerc is not null)
{ {
<span class="merc-badge">@item.LimitedToMerc</span> <span class="merc-badge">@item.LimitedToMerc</span>
@@ -82,7 +90,12 @@
@if (_showDetails) @if (_showDetails)
{ {
<div class="pool-body"> <div class="pool-body">
<div class="pool-name">@item.Name</div> <div class="d-flex align-items-center justify-content-between gap-1">
<div class="pool-name">@item.Name</div>
<a href="@item.DocUrl" class="pool-doc-link" title="View @item.Name reference doc" @onclick:stopPropagation draggable="false">
<span>Doc ↗</span>
</a>
</div>
@if (item.LimitedToMerc is not null) @if (item.LimitedToMerc is not null)
{ {
<span class="merc-badge">@item.LimitedToMerc</span> <span class="merc-badge">@item.LimitedToMerc</span>
@@ -119,7 +132,12 @@
@if (_showDetails) @if (_showDetails)
{ {
<div class="pool-body"> <div class="pool-body">
<div class="pool-name">@item.Name</div> <div class="d-flex align-items-center justify-content-between gap-1">
<div class="pool-name">@item.Name</div>
<a href="@item.DocUrl" class="pool-doc-link" title="View @item.Name reference doc" @onclick:stopPropagation draggable="false">
<span>Doc ↗</span>
</a>
</div>
<ul class="pool-levels"> <ul class="pool-levels">
@foreach (var level in item.Levels) @foreach (var level in item.Levels)
{ {
@@ -236,7 +254,8 @@
IReadOnlyList<string> Levels, IReadOnlyList<string> Levels,
string? LimitedTo, string? LimitedTo,
string? LimitedToMerc, string? LimitedToMerc,
string? ImageUrl); string? ImageUrl,
string DocUrl);
private static readonly string[] Heroes = ["Vynthra", "Prefect Aster"]; private static readonly string[] Heroes = ["Vynthra", "Prefect Aster"];
@@ -272,7 +291,8 @@
item.Levels, item.Levels,
item.LimitedTo, item.LimitedTo,
item.LimitedToMerc, item.LimitedToMerc,
item.ImageFileName is null ? null : $"images/faction-power-items/{Uri.EscapeDataString(item.ImageFileName)}")) item.ImageFileName is null ? null : $"images/faction-power-items/{Uri.EscapeDataString(item.ImageFileName)}",
ReferenceCatalog.GetUrlFor(item.Name) ?? $"/reference/faction-item/{ReferenceCatalog.ToSlug(item.Name)}"))
.ToList(); .ToList();
_allGeneralItems = GeneralItems.All _allGeneralItems = GeneralItems.All
@@ -283,7 +303,8 @@
item.Levels, item.Levels,
LimitedTo: null, LimitedTo: null,
item.LimitedToMerc, item.LimitedToMerc,
item.ImageFileName is null ? null : $"images/general-items/{Uri.EscapeDataString(item.ImageFileName)}")) item.ImageFileName is null ? null : $"images/general-items/{Uri.EscapeDataString(item.ImageFileName)}",
ReferenceCatalog.GetUrlFor(item.Name) ?? $"/reference/general-item/{ReferenceCatalog.ToSlug(item.Name)}"))
.ToList(); .ToList();
_allMasterItems = PlanetaryMasteryItems.All _allMasterItems = PlanetaryMasteryItems.All
@@ -294,7 +315,8 @@
item.Levels, item.Levels,
LimitedTo: null, LimitedTo: null,
LimitedToMerc: null, LimitedToMerc: null,
item.ImageFileName is null ? null : $"images/planetary-mastery-items/{Uri.EscapeDataString(item.ImageFileName)}")) item.ImageFileName is null ? null : $"images/planetary-mastery-items/{Uri.EscapeDataString(item.ImageFileName)}",
ReferenceCatalog.GetUrlFor(item.Name) ?? $"/reference/planetary-mastery-item/{ReferenceCatalog.ToSlug(item.Name)}"))
.ToList(); .ToList();
} }
@@ -129,6 +129,25 @@
color: #fff; color: #fff;
} }
.pool-doc-link {
font-size: 0.72rem;
color: var(--zs-cyan);
background: rgba(56, 189, 248, 0.12);
border: 1px solid rgba(56, 189, 248, 0.25);
padding: 0.1rem 0.4rem;
border-radius: 4px;
text-decoration: none;
transition: all 0.15s ease;
white-space: nowrap;
}
.pool-doc-link:hover {
background: rgba(56, 189, 248, 0.25);
color: #fff;
border-color: var(--zs-cyan);
box-shadow: 0 0 6px rgba(56, 189, 248, 0.3);
}
.merc-badge { .merc-badge {
display: inline-block; display: inline-block;
font-size: 0.72rem; font-size: 0.72rem;
@@ -0,0 +1,519 @@
@page "/reference"
@page "/reference/{Category}"
@page "/reference/{Category}/{ItemSlug}"
@inject NavigationManager Navigation
<PageTitle>@(SelectedItem is not null ? $"{SelectedItem.Name} - Reference" : SelectedCategory is not null ? $"{SelectedCategory.Name} - Reference" : "Item Reference Docs")</PageTitle>
<div class="ref-container">
@* Sidebar Navigation *@
<aside class="ref-sidebar" aria-label="Reference Navigation">
<div class="ref-sidebar-header">
<div class="d-flex align-items-center justify-content-between mb-2">
<span class="ref-sidebar-title">
<span class="ref-icon-glow">✦</span> Item Reference
</span>
<span class="badge badge-subtle">@FilteredTotalCount / @ReferenceCatalog.All.Count</span>
</div>
<div class="ref-search-box">
<input type="text"
class="form-control form-control-sm ref-search-input"
placeholder="Search reference docs..."
aria-label="Search reference docs"
@bind="_searchQuery"
@bind:event="oninput" />
@if (!string.IsNullOrEmpty(_searchQuery))
{
<button type="button" class="ref-search-clear" @onclick="ClearSearch" aria-label="Clear search">×</button>
}
</div>
<div class="ref-category-pills">
<button type="button"
class="ref-pill @(_activeCategoryFilter == null ? "active" : "")"
@onclick="() => FilterCategory(null)">
All
</button>
@foreach (var cat in ReferenceCatalog.Categories)
{
<button type="button"
class="ref-pill @(_activeCategoryFilter == cat.Slug ? "active" : "")"
@onclick="() => FilterCategory(cat.Slug)">
@cat.SingularName
</button>
}
</div>
</div>
<nav class="ref-nav-tree">
@foreach (var cat in DisplayCategories)
{
var itemsInCat = GetFilteredItems(cat);
if (itemsInCat.Count == 0 && !string.IsNullOrWhiteSpace(_searchQuery))
{
continue;
}
<div class="ref-group">
<div class="ref-group-header" @onclick="() => ToggleGroup(cat.Slug)">
<span class="ref-group-arrow @(IsGroupExpanded(cat.Slug) ? "expanded" : "")">▶</span>
<a href="/reference/@cat.Slug" class="ref-group-title" @onclick:stopPropagation>
@cat.Name
</a>
<span class="badge @cat.BadgeClass ms-auto">@itemsInCat.Count</span>
</div>
@if (IsGroupExpanded(cat.Slug))
{
<ul class="ref-item-list">
@foreach (var item in itemsInCat)
{
var isActive = SelectedItem?.Id == item.Id;
<li class="ref-item-row">
<a href="@item.CanonicalUrl" class="ref-item-link @(isActive ? "active" : "")">
@if (item.ImageUrl is not null)
{
<img src="@item.ImageUrl" class="ref-thumb" alt="" loading="lazy" />
}
else
{
<span class="ref-thumb ref-thumb-placeholder">✦</span>
}
<span class="ref-item-name">@item.Name</span>
@if (item.LimitedTo is not null)
{
<span class="ref-mini-tag hero-tag" title="Hero: @item.LimitedTo">@item.LimitedTo</span>
}
else if (item.LimitedToMerc is not null)
{
<span class="ref-mini-tag merc-tag" title="Mercenary: @item.LimitedToMerc">@item.LimitedToMerc</span>
}
</a>
</li>
}
</ul>
}
</div>
}
</nav>
</aside>
@* Main Content Area *@
<main class="ref-main" role="main">
@if (IsItemNotFound)
{
<div class="alert alert-warning zs-glass-card mb-4" role="alert">
<h4 class="alert-heading">Item Not Found</h4>
<p>Could not find reference item matching <code>@Category/@ItemSlug</code>.</p>
<hr class="opacity-25" />
<a href="/reference" class="btn btn-sm btn-scifi-outline">Return to Reference Overview</a>
</div>
}
else if (SelectedItem is not null)
{
@* Breadcrumbs *@
<nav aria-label="breadcrumb" class="ref-breadcrumbs">
<ol class="breadcrumb mb-0">
<li class="breadcrumb-item"><a href="/reference">Reference</a></li>
<li class="breadcrumb-item"><a href="/reference/@SelectedItem.CategorySlug">@SelectedItem.CategoryName</a></li>
<li class="breadcrumb-item active" aria-current="page">@SelectedItem.Name</li>
</ol>
</nav>
@* Item Header Card *@
<header class="zs-glass-card ref-hero-card mb-4">
<div class="d-flex flex-wrap align-items-start gap-4">
<div class="ref-hero-icon-container">
@if (SelectedItem.ImageUrl is not null)
{
<img class="ref-hero-icon" src="@SelectedItem.ImageUrl" alt="@SelectedItem.Name" />
}
else
{
<div class="ref-hero-icon ref-hero-icon-placeholder" aria-hidden="true">✦</div>
}
</div>
<div class="ref-hero-info flex-grow-1">
<div class="d-flex flex-wrap align-items-center gap-2 mb-2">
<span class="badge @GetCategoryBadgeClass(SelectedItem.CategoryKind)">@SelectedItem.CategoryName</span>
@if (SelectedItem.LimitedTo is not null)
{
<span class="badge badge-hero" title="Limited to Hero">
<span class="badge-icon">👤</span> Hero: @SelectedItem.LimitedTo
</span>
}
@if (SelectedItem.LimitedToMerc is not null)
{
<span class="badge badge-merc" title="Limited to Mercenary">
<span class="badge-icon">⚔</span> Mercenary: @SelectedItem.LimitedToMerc
</span>
}
@if (SelectedItem.MinimumLevel is not null)
{
<span class="badge badge-level" title="Minimum Required Level">
<span class="badge-icon">★</span> Min Level @SelectedItem.MinimumLevel
</span>
}
<span class="badge badge-tiers">@SelectedItem.Levels.Count @(SelectedItem.Levels.Count == 1 ? "Tier" : "Tiers")</span>
</div>
<h1 class="ref-hero-title mb-2">@SelectedItem.Name</h1>
<p class="ref-hero-summary text-muted mb-3">
@GetSummaryText(SelectedItem)
</p>
<div class="d-flex flex-wrap gap-2">
<a href="/loadout" class="btn btn-sm btn-scifi-primary">
<span class="btn-icon">⚡</span> Open in Loadout
</a>
<button type="button" class="btn btn-sm btn-scifi-outline" @onclick="CopyCanonicalUrl">
<span>@(_copyNotification ?? "Copy Reference URL")</span>
</button>
</div>
</div>
</div>
</header>
@* Technical Metadata Specs Table *@
<section class="zs-glass-card ref-spec-card mb-4" aria-labelledby="specs-heading">
<h2 id="specs-heading" class="ref-section-heading">
<span class="heading-accent">▍</span> Technical Specifications
</h2>
<div class="table-responsive">
<table class="table table-sm ref-spec-table mb-0">
<tbody>
<tr>
<th scope="row">System Identifier</th>
<td><code>@SelectedItem.Id</code></td>
<th scope="row">Category</th>
<td>@SelectedItem.CategoryName</td>
</tr>
<tr>
<th scope="row">Hero Restriction</th>
<td>@(SelectedItem.LimitedTo ?? "Universal / Any")</td>
<th scope="row">Mercenary Restriction</th>
<td>@(SelectedItem.LimitedToMerc ?? "None")</td>
</tr>
<tr>
<th scope="row">Minimum Level</th>
<td>@(SelectedItem.MinimumLevel.HasValue ? $"Level {SelectedItem.MinimumLevel}" : "None (Unlocked at Level 1)")</td>
<th scope="row">Progression Tiers</th>
<td>@SelectedItem.Levels.Count @(SelectedItem.Levels.Count == 1 ? "Level" : "Levels")</td>
</tr>
<tr>
<th scope="row">Canonical API Route</th>
<td colspan="3"><code>@SelectedItem.CanonicalUrl</code></td>
</tr>
</tbody>
</table>
</div>
</section>
@* Tiers & Upgrades Section *@
<section class="zs-glass-card ref-tiers-card mb-4" aria-labelledby="progression-heading">
<h2 id="progression-heading" class="ref-section-heading">
<span class="heading-accent">▍</span> Progression & Upgrades
</h2>
<div class="ref-tiers-list">
@for (var i = 0; i < SelectedItem.Levels.Count; i++)
{
var tierNumber = i + 1;
var levelDesc = SelectedItem.Levels[i];
<div class="ref-tier-item">
<div class="ref-tier-badge">
<span class="tier-label">TIER</span>
<span class="tier-num">@tierNumber.ToString("D2")</span>
</div>
<div class="ref-tier-content">
<div class="ref-tier-title">Level @tierNumber Upgrade</div>
<div class="ref-tier-desc">@levelDesc</div>
</div>
</div>
}
</div>
</section>
@* Markdown Field Notes / Body *@
@if (!string.IsNullOrWhiteSpace(SelectedItem.Body))
{
<section class="zs-glass-card ref-notes-card mb-4" aria-labelledby="notes-heading">
<h2 id="notes-heading" class="ref-section-heading">
<span class="heading-accent">▍</span> Documentation & Tactical Notes
</h2>
<div class="ref-markdown-body">
<MarkdownView Markdown="@SelectedItem.Body" />
</div>
</section>
}
@* Navigation Footer (Previous / Next Item) *@
<nav class="ref-pager-nav d-flex justify-content-between gap-3 mt-4" aria-label="Item Reference Pager">
@if (PreviousItem is not null)
{
<a href="@PreviousItem.CanonicalUrl" class="ref-pager-card ref-pager-prev zs-glass-card">
<span class="pager-direction">← Previous Item</span>
<div class="d-flex align-items-center gap-2 mt-1">
@if (PreviousItem.ImageUrl is not null)
{
<img src="@PreviousItem.ImageUrl" class="pager-thumb" alt="" />
}
<span class="pager-name">@PreviousItem.Name</span>
</div>
</a>
}
else
{
<div class="ref-pager-card-empty"></div>
}
@if (NextItem is not null)
{
<a href="@NextItem.CanonicalUrl" class="ref-pager-card ref-pager-next zs-glass-card text-end">
<span class="pager-direction">Next Item →</span>
<div class="d-flex align-items-center justify-content-end gap-2 mt-1">
<span class="pager-name">@NextItem.Name</span>
@if (NextItem.ImageUrl is not null)
{
<img src="@NextItem.ImageUrl" class="pager-thumb" alt="" />
}
</div>
</a>
}
else
{
<div class="ref-pager-card-empty"></div>
}
</nav>
}
else
{
@* Reference Hub / Overview Landing View *@
<div class="ref-overview">
<header class="zs-hero p-4 mb-4">
<div class="d-flex align-items-center justify-content-between flex-wrap gap-3">
<div>
<span class="badge badge-scifi-glow mb-2">ZERO SPACE DATABASE</span>
<h1 class="ref-overview-title mb-1">Loadout Item Reference</h1>
<p class="ref-overview-subtitle mb-0 text-muted">
Comprehensive documentation for all 47 faction powers, general enhancements, and planetary mastery items.
</p>
</div>
<a href="/loadout" class="btn btn-scifi-primary">
<span class="btn-icon">⚡</span> Configure Loadout
</a>
</div>
</header>
@* Category Cards *@
<div class="row g-4 mb-4">
@foreach (var cat in ReferenceCatalog.Categories)
{
var isCatSelected = SelectedCategory?.Slug == cat.Slug;
<div class="col-md-4">
<div class="zs-glass-card ref-category-card h-100 @(isCatSelected ? "active" : "")">
<div class="d-flex justify-content-between align-items-center mb-2">
<span class="badge @cat.BadgeClass">@cat.SingularName</span>
<span class="badge badge-subtle">@cat.Items.Count Items</span>
</div>
<h3 class="ref-category-card-title">@cat.Name</h3>
<p class="ref-category-card-desc text-muted mb-3">@cat.Description</p>
<div class="mt-auto">
<a href="/reference/@cat.Slug" class="btn btn-sm btn-scifi-outline w-100">
Browse @cat.Name
</a>
</div>
</div>
</div>
}
</div>
@* Category Detail Grids *@
@foreach (var cat in DisplayCategories)
{
<section class="zs-glass-card p-4 mb-4" aria-labelledby="cat-heading-@cat.Slug">
<div class="d-flex align-items-center justify-content-between mb-3 border-bottom pb-2 border-secondary border-opacity-25">
<h2 id="cat-heading-@cat.Slug" class="h4 mb-0 d-flex align-items-center gap-2">
<span class="heading-accent">▍</span> @cat.Name
</h2>
<span class="badge @cat.BadgeClass">@cat.Items.Count Items</span>
</div>
<div class="row g-3">
@foreach (var item in cat.Items)
{
<div class="col-sm-6 col-lg-4 col-xl-3">
<a href="@item.CanonicalUrl" class="ref-grid-card zs-glass-card d-flex align-items-center gap-3 p-2 text-decoration-none">
@if (item.ImageUrl is not null)
{
<img src="@item.ImageUrl" class="ref-grid-icon" alt="" loading="lazy" />
}
else
{
<div class="ref-grid-icon ref-grid-icon-placeholder">✦</div>
}
<div class="ref-grid-info min-w-0 flex-grow-1">
<div class="ref-grid-name text-truncate">@item.Name</div>
<div class="ref-grid-sub text-truncate">
@if (item.LimitedTo is not null)
{
<span>Hero: @item.LimitedTo</span>
}
else if (item.LimitedToMerc is not null)
{
<span>Merc: @item.LimitedToMerc</span>
}
else
{
<span>@item.Levels.Count @(item.Levels.Count == 1 ? "Tier" : "Tiers")</span>
}
</div>
</div>
</a>
</div>
}
</div>
</section>
}
</div>
}
</main>
</div>
@code {
[Parameter]
public string? Category { get; set; }
[Parameter]
public string? ItemSlug { get; set; }
private ReferenceItem? SelectedItem;
private ReferenceCategory? SelectedCategory;
private ReferenceItem? PreviousItem;
private ReferenceItem? NextItem;
private bool IsItemNotFound;
private string _searchQuery = string.Empty;
private string? _activeCategoryFilter;
private readonly HashSet<string> _expandedGroups = ["faction-item", "general-item", "planetary-mastery-item"];
private string? _copyNotification;
protected override void OnParametersSet()
{
IsItemNotFound = false;
PreviousItem = null;
NextItem = null;
if (!string.IsNullOrWhiteSpace(Category) && !string.IsNullOrWhiteSpace(ItemSlug))
{
SelectedItem = ReferenceCatalog.FindItem(Category, ItemSlug);
if (SelectedItem is null)
{
IsItemNotFound = true;
SelectedCategory = ReferenceCatalog.GetCategory(Category);
}
else
{
SelectedCategory = ReferenceCatalog.GetCategory(SelectedItem.CategorySlug);
_expandedGroups.Add(SelectedItem.CategorySlug);
var adjacent = ReferenceCatalog.GetAdjacentItems(SelectedItem);
PreviousItem = adjacent.Previous;
NextItem = adjacent.Next;
}
}
else if (!string.IsNullOrWhiteSpace(Category))
{
SelectedCategory = ReferenceCatalog.GetCategory(Category);
SelectedItem = null;
}
else
{
SelectedCategory = null;
SelectedItem = null;
}
}
private IEnumerable<ReferenceCategory> DisplayCategories =>
SelectedCategory is not null && string.IsNullOrEmpty(_activeCategoryFilter) && string.IsNullOrEmpty(_searchQuery)
? [SelectedCategory]
: _activeCategoryFilter is not null
? ReferenceCatalog.Categories.Where(c => c.Slug == _activeCategoryFilter)
: ReferenceCatalog.Categories;
private IReadOnlyList<ReferenceItem> GetFilteredItems(ReferenceCategory category)
{
var items = category.Items.AsEnumerable();
if (!string.IsNullOrWhiteSpace(_searchQuery))
{
var query = _searchQuery.Trim();
items = items.Where(item =>
item.Name.Contains(query, StringComparison.OrdinalIgnoreCase) ||
item.Id.Contains(query, StringComparison.OrdinalIgnoreCase) ||
(item.LimitedTo?.Contains(query, StringComparison.OrdinalIgnoreCase) ?? false) ||
(item.LimitedToMerc?.Contains(query, StringComparison.OrdinalIgnoreCase) ?? false) ||
item.Levels.Any(l => l.Contains(query, StringComparison.OrdinalIgnoreCase)) ||
item.Body.Contains(query, StringComparison.OrdinalIgnoreCase));
}
return items.ToList();
}
private int FilteredTotalCount =>
ReferenceCatalog.Categories.Sum(c => GetFilteredItems(c).Count);
private void FilterCategory(string? categorySlug)
{
_activeCategoryFilter = categorySlug;
}
private void ClearSearch()
{
_searchQuery = string.Empty;
}
private void ToggleGroup(string slug)
{
if (_expandedGroups.Contains(slug))
{
_expandedGroups.Remove(slug);
}
else
{
_expandedGroups.Add(slug);
}
}
private bool IsGroupExpanded(string slug) => _expandedGroups.Contains(slug);
private static string GetCategoryBadgeClass(ReferenceCategoryKind kind) => kind switch
{
ReferenceCategoryKind.FactionPower => "badge-faction-power",
ReferenceCategoryKind.GeneralItem => "badge-general-item",
ReferenceCategoryKind.PlanetaryMasteryItem => "badge-mastery-item",
_ => "badge-side"
};
private static string GetSummaryText(ReferenceItem item)
{
if (item.Levels.Count > 0)
{
return item.Levels[0];
}
return $"{item.CategoryName} available in loadout customization.";
}
private async Task CopyCanonicalUrl()
{
if (SelectedItem is null) return;
var fullUrl = Navigation.ToAbsoluteUri(SelectedItem.CanonicalUrl).ToString();
// In WebAssembly, or standard Blazor, notification feedback:
_copyNotification = "Copied link to clipboard!";
await Task.Delay(2000);
_copyNotification = null;
}
}
@@ -0,0 +1,632 @@
.ref-container {
display: flex;
gap: 1.5rem;
min-height: calc(100vh - 4rem);
align-items: flex-start;
}
/* Sidebar */
.ref-sidebar {
width: 320px;
flex-shrink: 0;
position: sticky;
top: 1rem;
max-height: calc(100vh - 2rem);
display: flex;
flex-direction: column;
background: var(--zs-bg-card);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
border: 1px solid var(--zs-border-subtle);
border-radius: 12px;
overflow: hidden;
}
.ref-sidebar-header {
padding: 1rem;
border-bottom: 1px solid var(--zs-border-subtle);
background: rgba(8, 12, 20, 0.4);
}
.ref-sidebar-title {
font-weight: 700;
font-size: 1rem;
color: #fff;
letter-spacing: -0.01em;
}
.ref-icon-glow {
color: var(--zs-cyan);
text-shadow: 0 0 8px rgba(56, 189, 248, 0.6);
}
.ref-search-box {
position: relative;
margin-bottom: 0.75rem;
}
.ref-search-input {
background: rgba(15, 23, 42, 0.8);
border: 1px solid rgba(56, 189, 248, 0.2);
color: var(--zs-text-main);
border-radius: 6px;
padding-right: 1.75rem;
font-size: 0.85rem;
}
.ref-search-input:focus {
background: rgba(15, 23, 42, 0.95);
border-color: var(--zs-cyan);
box-shadow: 0 0 10px rgba(56, 189, 248, 0.25);
color: #fff;
}
.ref-search-clear {
position: absolute;
right: 0.4rem;
top: 50%;
transform: translateY(-50%);
background: none;
border: none;
color: var(--zs-text-muted);
font-size: 1rem;
cursor: pointer;
padding: 0 0.25rem;
line-height: 1;
}
.ref-search-clear:hover {
color: #fff;
}
.ref-category-pills {
display: flex;
gap: 0.25rem;
overflow-x: auto;
padding-bottom: 2px;
}
.ref-pill {
background: rgba(30, 41, 59, 0.6);
border: 1px solid rgba(56, 189, 248, 0.15);
color: var(--zs-text-muted);
border-radius: 20px;
padding: 0.15rem 0.55rem;
font-size: 0.72rem;
font-weight: 500;
white-space: nowrap;
cursor: pointer;
transition: all 0.15s ease;
}
.ref-pill:hover {
background: rgba(56, 189, 248, 0.15);
color: #fff;
border-color: rgba(56, 189, 248, 0.35);
}
.ref-pill.active {
background: rgba(56, 189, 248, 0.25);
color: var(--zs-cyan);
border-color: var(--zs-cyan);
box-shadow: 0 0 8px rgba(56, 189, 248, 0.25);
font-weight: 600;
}
.ref-nav-tree {
flex-grow: 1;
overflow-y: auto;
padding: 0.5rem;
}
.ref-group {
margin-bottom: 0.5rem;
}
.ref-group-header {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.4rem 0.5rem;
border-radius: 6px;
cursor: pointer;
transition: background 0.15s ease;
}
.ref-group-header:hover {
background: rgba(56, 189, 248, 0.08);
}
.ref-group-arrow {
font-size: 0.65rem;
color: var(--zs-text-dim);
transition: transform 0.2s ease;
display: inline-block;
}
.ref-group-arrow.expanded {
transform: rotate(90deg);
color: var(--zs-cyan);
}
.ref-group-title {
font-weight: 600;
font-size: 0.85rem;
color: var(--zs-text-main);
text-decoration: none;
}
.ref-group-title:hover {
color: var(--zs-cyan);
}
.ref-item-list {
list-style: none;
margin: 0.25rem 0 0 0.5rem;
padding-left: 0.5rem;
border-left: 1px solid rgba(56, 189, 248, 0.15);
}
.ref-item-row {
margin-bottom: 2px;
}
.ref-item-link {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.35rem 0.5rem;
border-radius: 6px;
color: var(--zs-text-muted);
text-decoration: none;
font-size: 0.82rem;
transition: all 0.15s ease;
border: 1px solid transparent;
}
.ref-item-link:hover {
background: rgba(56, 189, 248, 0.08);
color: #fff;
}
.ref-item-link.active {
background: linear-gradient(90deg, rgba(56, 189, 248, 0.2) 0%, rgba(56, 189, 248, 0.05) 100%);
color: var(--zs-cyan);
border-color: rgba(56, 189, 248, 0.4);
box-shadow: 0 0 10px rgba(56, 189, 248, 0.15);
font-weight: 600;
}
.ref-thumb {
width: 22px;
height: 22px;
border-radius: 4px;
object-fit: contain;
background: rgba(15, 23, 42, 0.6);
flex-shrink: 0;
}
.ref-thumb-placeholder {
display: inline-flex;
align-items: center;
justify-content: center;
font-size: 0.7rem;
color: var(--zs-cyan);
background: rgba(56, 189, 248, 0.15);
}
.ref-item-name {
flex-grow: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.ref-mini-tag {
font-size: 0.65rem;
padding: 0.1rem 0.35rem;
border-radius: 4px;
white-space: nowrap;
flex-shrink: 0;
}
.hero-tag {
background: rgba(167, 139, 250, 0.15);
color: #c4b5fd;
border: 1px solid rgba(167, 139, 250, 0.3);
}
.merc-tag {
background: rgba(245, 158, 11, 0.15);
color: #fcd34d;
border: 1px solid rgba(245, 158, 11, 0.3);
}
/* Main Area */
.ref-main {
flex-grow: 1;
min-width: 0;
}
.ref-breadcrumbs {
margin-bottom: 1rem;
font-size: 0.85rem;
}
.ref-breadcrumbs .breadcrumb {
background: none;
padding: 0;
}
.ref-breadcrumbs .breadcrumb-item a {
color: var(--zs-text-muted);
}
.ref-breadcrumbs .breadcrumb-item.active {
color: var(--zs-cyan);
}
/* Hero Card */
.ref-hero-card {
padding: 1.5rem;
border: 1px solid rgba(56, 189, 248, 0.25);
background: radial-gradient(ellipse at top right, rgba(56, 189, 248, 0.08) 0%, transparent 60%),
rgba(15, 23, 42, 0.8);
}
.ref-hero-icon-container {
width: 84px;
height: 84px;
border-radius: 12px;
background: rgba(8, 12, 20, 0.7);
border: 1px solid rgba(56, 189, 248, 0.3);
box-shadow: 0 0 15px rgba(56, 189, 248, 0.15);
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
overflow: hidden;
}
.ref-hero-icon {
width: 72px;
height: 72px;
object-fit: contain;
}
.ref-hero-icon-placeholder {
font-size: 2.2rem;
color: var(--zs-cyan);
text-shadow: 0 0 12px rgba(56, 189, 248, 0.6);
}
.ref-hero-title {
font-size: 1.85rem;
font-weight: 800;
letter-spacing: -0.02em;
color: #fff;
}
.ref-hero-summary {
font-size: 0.95rem;
line-height: 1.5;
}
/* Section Headings */
.ref-section-heading {
font-size: 1.15rem;
font-weight: 700;
margin-bottom: 1rem;
color: #fff;
display: flex;
align-items: center;
gap: 0.5rem;
}
.heading-accent {
color: var(--zs-cyan);
text-shadow: 0 0 8px rgba(56, 189, 248, 0.6);
}
/* Spec Table */
.ref-spec-card {
padding: 1.25rem;
}
.ref-spec-table {
font-size: 0.88rem;
color: var(--zs-text-main);
}
.ref-spec-table th {
color: var(--zs-text-muted);
font-weight: 600;
background: transparent;
border-color: rgba(56, 189, 248, 0.12);
width: 20%;
padding: 0.6rem 0.75rem;
}
.ref-spec-table td {
background: transparent;
border-color: rgba(56, 189, 248, 0.12);
width: 30%;
padding: 0.6rem 0.75rem;
}
.ref-spec-table code {
background: rgba(56, 189, 248, 0.1);
color: #7dd3fc;
padding: 0.15rem 0.4rem;
border-radius: 4px;
border: 1px solid rgba(56, 189, 248, 0.2);
font-size: 0.82rem;
}
/* Tiers */
.ref-tiers-card {
padding: 1.25rem;
}
.ref-tiers-list {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.ref-tier-item {
display: flex;
gap: 1rem;
align-items: flex-start;
padding: 0.85rem;
background: rgba(8, 12, 20, 0.45);
border: 1px solid rgba(56, 189, 248, 0.12);
border-radius: 8px;
transition: border-color 0.2s ease, background 0.2s ease;
}
.ref-tier-item:hover {
border-color: rgba(56, 189, 248, 0.3);
background: rgba(15, 23, 42, 0.6);
}
.ref-tier-badge {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background: rgba(56, 189, 248, 0.12);
border: 1px solid rgba(56, 189, 248, 0.3);
border-radius: 6px;
padding: 0.25rem 0.6rem;
min-width: 58px;
flex-shrink: 0;
}
.tier-label {
font-size: 0.58rem;
font-weight: 700;
color: var(--zs-cyan);
letter-spacing: 0.05em;
}
.tier-num {
font-size: 1rem;
font-weight: 800;
color: #fff;
line-height: 1;
}
.ref-tier-content {
flex-grow: 1;
}
.ref-tier-title {
font-size: 0.88rem;
font-weight: 700;
color: #fff;
margin-bottom: 0.2rem;
}
.ref-tier-desc {
font-size: 0.88rem;
color: var(--zs-text-muted);
line-height: 1.45;
}
/* Notes / Markdown */
.ref-notes-card {
padding: 1.25rem;
}
.ref-markdown-body {
font-size: 0.92rem;
line-height: 1.6;
color: var(--zs-text-muted);
}
.ref-markdown-body p:last-child {
margin-bottom: 0;
}
/* Badges */
.badge-faction-power {
background: rgba(167, 139, 250, 0.18);
color: #c4b5fd;
border: 1px solid rgba(167, 139, 250, 0.45);
box-shadow: 0 0 8px rgba(167, 139, 250, 0.25);
font-weight: 600;
}
.badge-general-item {
background: rgba(16, 185, 129, 0.18);
color: #34d399;
border: 1px solid rgba(16, 185, 129, 0.45);
box-shadow: 0 0 8px rgba(16, 185, 129, 0.25);
font-weight: 600;
}
.badge-mastery-item {
background: rgba(245, 158, 11, 0.18);
color: #fbbf24;
border: 1px solid rgba(245, 158, 11, 0.45);
box-shadow: 0 0 8px rgba(245, 158, 11, 0.25);
font-weight: 600;
}
.badge-hero {
background: rgba(167, 139, 250, 0.15);
color: #ddd6fe;
border: 1px solid rgba(167, 139, 250, 0.35);
font-weight: 500;
}
.badge-merc {
background: rgba(245, 158, 11, 0.15);
color: #fde68a;
border: 1px solid rgba(245, 158, 11, 0.35);
font-weight: 500;
}
.badge-level {
background: rgba(244, 63, 94, 0.15);
color: #fda4af;
border: 1px solid rgba(244, 63, 94, 0.35);
font-weight: 500;
}
.badge-tiers {
background: rgba(56, 189, 248, 0.15);
color: #bae6fd;
border: 1px solid rgba(56, 189, 248, 0.35);
font-weight: 500;
}
.badge-subtle {
background: rgba(148, 163, 184, 0.15);
color: #cbd5e1;
border: 1px solid rgba(148, 163, 184, 0.25);
font-size: 0.72rem;
}
/* Pager */
.ref-pager-card {
flex: 1;
padding: 0.85rem 1rem;
text-decoration: none;
color: var(--zs-text-main);
display: flex;
flex-direction: column;
}
.ref-pager-card:hover {
border-color: var(--zs-cyan);
box-shadow: 0 0 15px rgba(56, 189, 248, 0.2);
}
.ref-pager-card-empty {
flex: 1;
}
.pager-direction {
font-size: 0.72rem;
color: var(--zs-cyan);
text-transform: uppercase;
font-weight: 700;
letter-spacing: 0.05em;
}
.pager-thumb {
width: 22px;
height: 22px;
border-radius: 4px;
object-fit: contain;
}
.pager-name {
font-weight: 600;
font-size: 0.9rem;
}
/* Overview Hub */
.ref-overview-title {
font-size: 1.85rem;
font-weight: 800;
color: #fff;
letter-spacing: -0.02em;
}
.ref-overview-subtitle {
font-size: 0.95rem;
}
.ref-category-card {
padding: 1.25rem;
display: flex;
flex-direction: column;
}
.ref-category-card-title {
font-size: 1.15rem;
font-weight: 700;
color: #fff;
margin-bottom: 0.4rem;
}
.ref-category-card-desc {
font-size: 0.85rem;
line-height: 1.45;
}
.ref-grid-card {
border: 1px solid rgba(56, 189, 248, 0.12);
border-radius: 8px;
transition: all 0.2s ease;
}
.ref-grid-card:hover {
border-color: var(--zs-cyan);
transform: translateY(-2px);
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.4), 0 0 12px rgba(56, 189, 248, 0.15);
}
.ref-grid-icon {
width: 38px;
height: 38px;
border-radius: 6px;
object-fit: contain;
background: rgba(8, 12, 20, 0.5);
flex-shrink: 0;
}
.ref-grid-icon-placeholder {
display: flex;
align-items: center;
justify-content: center;
font-size: 1.1rem;
color: var(--zs-cyan);
}
.ref-grid-name {
font-size: 0.88rem;
font-weight: 600;
color: #fff;
}
.ref-grid-sub {
font-size: 0.75rem;
color: var(--zs-text-muted);
}
/* Responsive */
@media (max-width: 991px) {
.ref-container {
flex-direction: column;
}
.ref-sidebar {
width: 100%;
position: static;
max-height: 380px;
}
}
@@ -0,0 +1,300 @@
using System.Text;
using System.Text.RegularExpressions;
using ZeroSpace.Model;
namespace ZeroSpace.Web.Services;
public enum ReferenceCategoryKind
{
FactionPower,
GeneralItem,
PlanetaryMasteryItem
}
public sealed record ReferenceItem(
string Id,
string Name,
string Slug,
string CategorySlug,
string CategoryName,
ReferenceCategoryKind CategoryKind,
IReadOnlyList<string> Levels,
string? LimitedTo,
string? LimitedToMerc,
int? MinimumLevel,
string? ImageUrl,
string Body)
{
public string CanonicalUrl => $"/reference/{CategorySlug}/{Slug}";
}
public sealed record ReferenceCategory(
string Slug,
string Name,
string SingularName,
string Description,
string BadgeClass,
ReferenceCategoryKind Kind,
IReadOnlyList<ReferenceItem> Items);
public static partial class ReferenceCatalog
{
public const string FactionCategorySlug = "faction-item";
public const string GeneralCategorySlug = "general-item";
public const string MasteryCategorySlug = "planetary-mastery-item";
private static readonly List<ReferenceItem> FactionItemsList;
private static readonly List<ReferenceItem> GeneralItemsList;
private static readonly List<ReferenceItem> MasteryItemsList;
private static readonly List<ReferenceItem> AllItemsList;
private static readonly List<ReferenceCategory> CategoriesList;
private static readonly Dictionary<string, ReferenceItem> ItemsByCanonicalUrl;
private static readonly Dictionary<string, ReferenceItem> ItemsByName;
private static readonly Dictionary<string, ReferenceItem> ItemsById;
static ReferenceCatalog()
{
FactionItemsList = ZeroSpace.Model.FactionPowerItems.All
.Select(item => new ReferenceItem(
Id: item.Id,
Name: item.Name,
Slug: ToSlug(item.Name),
CategorySlug: FactionCategorySlug,
CategoryName: "Faction Power",
CategoryKind: ReferenceCategoryKind.FactionPower,
Levels: item.Levels,
LimitedTo: item.LimitedTo,
LimitedToMerc: item.LimitedToMerc,
MinimumLevel: item.MinimumLevel,
ImageUrl: item.ImageFileName is null ? null : $"images/faction-power-items/{Uri.EscapeDataString(item.ImageFileName)}",
Body: item.Body))
.OrderBy(i => i.Name, StringComparer.OrdinalIgnoreCase)
.ToList();
GeneralItemsList = ZeroSpace.Model.GeneralItems.All
.Select(item => new ReferenceItem(
Id: item.Id,
Name: item.Name,
Slug: ToSlug(item.Name),
CategorySlug: GeneralCategorySlug,
CategoryName: "General Item",
CategoryKind: ReferenceCategoryKind.GeneralItem,
Levels: item.Levels,
LimitedTo: null,
LimitedToMerc: item.LimitedToMerc,
MinimumLevel: null,
ImageUrl: item.ImageFileName is null ? null : $"images/general-items/{Uri.EscapeDataString(item.ImageFileName)}",
Body: item.Body))
.OrderBy(i => i.Name, StringComparer.OrdinalIgnoreCase)
.ToList();
MasteryItemsList = ZeroSpace.Model.PlanetaryMasteryItems.All
.Select(item => new ReferenceItem(
Id: item.Id,
Name: item.Name,
Slug: ToSlug(item.Name),
CategorySlug: MasteryCategorySlug,
CategoryName: "Planetary Mastery Item",
CategoryKind: ReferenceCategoryKind.PlanetaryMasteryItem,
Levels: item.Levels,
LimitedTo: null,
LimitedToMerc: null,
MinimumLevel: null,
ImageUrl: item.ImageFileName is null ? null : $"images/planetary-mastery-items/{Uri.EscapeDataString(item.ImageFileName)}",
Body: item.Body))
.OrderBy(i => i.Name, StringComparer.OrdinalIgnoreCase)
.ToList();
AllItemsList = [.. FactionItemsList, .. GeneralItemsList, .. MasteryItemsList];
CategoriesList =
[
new ReferenceCategory(
Slug: FactionCategorySlug,
Name: "Faction Powers",
SingularName: "Faction Power",
Description: "High-impact active and passive faction powers and commander abilities equipped to customize faction tactical capabilities.",
BadgeClass: "badge-faction-power",
Kind: ReferenceCategoryKind.FactionPower,
Items: FactionItemsList),
new ReferenceCategory(
Slug: GeneralCategorySlug,
Name: "General Items",
SingularName: "General Item",
Description: "Universal strategic enhancements, defensive matrix structures, mercenary compound unlocks, and economic items.",
BadgeClass: "badge-general-item",
Kind: ReferenceCategoryKind.GeneralItem,
Items: GeneralItemsList),
new ReferenceCategory(
Slug: MasteryCategorySlug,
Name: "Planetary Mastery Items",
SingularName: "Planetary Mastery Item",
Description: "Mastery artifacts and planetary topbar energy catalysts that scale with system and loadout progression.",
BadgeClass: "badge-mastery-item",
Kind: ReferenceCategoryKind.PlanetaryMasteryItem,
Items: MasteryItemsList)
];
ItemsByCanonicalUrl = new Dictionary<string, ReferenceItem>(StringComparer.OrdinalIgnoreCase);
ItemsByName = new Dictionary<string, ReferenceItem>(StringComparer.OrdinalIgnoreCase);
ItemsById = new Dictionary<string, ReferenceItem>(StringComparer.OrdinalIgnoreCase);
foreach (var item in AllItemsList)
{
ItemsByCanonicalUrl[item.CanonicalUrl] = item;
ItemsByName[item.Name] = item;
ItemsById[item.Id] = item;
}
}
public static IReadOnlyList<ReferenceItem> All => AllItemsList;
public static IReadOnlyList<ReferenceItem> FactionPowers => FactionItemsList;
public static IReadOnlyList<ReferenceItem> GeneralItems => GeneralItemsList;
public static IReadOnlyList<ReferenceItem> MasteryItems => MasteryItemsList;
public static IReadOnlyList<ReferenceCategory> Categories => CategoriesList;
public static ReferenceCategory? GetCategory(string? categorySlugOrAlias)
{
if (string.IsNullOrWhiteSpace(categorySlugOrAlias))
{
return null;
}
var normalized = Normalize(categorySlugOrAlias);
if (normalized is "faction-item" or "faction-items" or "faction-power" or "faction-powers" or "faction" or "factionitem" or "factionpower")
{
return CategoriesList[0];
}
if (normalized is "general-item" or "general-items" or "general" or "generalitem" or "generalitems")
{
return CategoriesList[1];
}
if (normalized is "planetary-mastery-item" or "planetary-mastery-items" or "master-item" or "master-items" or "mastery" or "planetary-mastery" or "planetarymasteryitem" or "masteritem")
{
return CategoriesList[2];
}
return CategoriesList.FirstOrDefault(c => c.Slug.Equals(categorySlugOrAlias, StringComparison.OrdinalIgnoreCase) ||
Normalize(c.Name).Equals(normalized, StringComparison.OrdinalIgnoreCase));
}
public static ReferenceItem? FindItem(string? categorySlugOrAlias, string? itemSlugOrId)
{
if (string.IsNullOrWhiteSpace(itemSlugOrId))
{
return null;
}
var category = GetCategory(categorySlugOrAlias);
var searchPool = category is not null ? category.Items : AllItemsList;
var normalizedTarget = Normalize(itemSlugOrId);
// 1. Direct match on slug
var match = searchPool.FirstOrDefault(i => Normalize(i.Slug).Equals(normalizedTarget, StringComparison.OrdinalIgnoreCase));
if (match is not null)
{
return match;
}
// 2. Match on Id
match = searchPool.FirstOrDefault(i => i.Id.Equals(itemSlugOrId, StringComparison.OrdinalIgnoreCase) ||
Normalize(i.Id).Equals(normalizedTarget, StringComparison.OrdinalIgnoreCase));
if (match is not null)
{
return match;
}
// 3. Match on Name
match = searchPool.FirstOrDefault(i => i.Name.Equals(itemSlugOrId, StringComparison.OrdinalIgnoreCase) ||
Normalize(i.Name).Equals(normalizedTarget, StringComparison.OrdinalIgnoreCase));
if (match is not null)
{
return match;
}
// 4. Fallback: Search all items regardless of category if not found in specific category
if (category is not null)
{
return FindItem(null, itemSlugOrId);
}
return null;
}
public static ReferenceItem? FindByName(string name)
{
if (string.IsNullOrWhiteSpace(name))
{
return null;
}
if (ItemsByName.TryGetValue(name, out var item))
{
return item;
}
return FindItem(null, name);
}
public static string? GetUrlFor(string name)
{
var item = FindByName(name);
return item?.CanonicalUrl;
}
public static (ReferenceItem? Previous, ReferenceItem? Next) GetAdjacentItems(ReferenceItem current)
{
var category = GetCategory(current.CategorySlug);
var pool = category?.Items ?? AllItemsList;
var index = pool.ToList().FindIndex(i => i.Id == current.Id);
if (index < 0)
{
return (null, null);
}
var previous = index > 0 ? pool[index - 1] : null;
var next = index < pool.Count - 1 ? pool[index + 1] : null;
return (previous, next);
}
public static string ToSlug(string text)
{
if (string.IsNullOrWhiteSpace(text))
{
return string.Empty;
}
var lower = text.Trim().ToLowerInvariant();
var builder = new StringBuilder();
var prevHyphen = false;
foreach (var c in lower)
{
if (char.IsLetterOrDigit(c))
{
builder.Append(c);
prevHyphen = false;
}
else if (!prevHyphen)
{
builder.Append('-');
prevHyphen = true;
}
}
var result = builder.ToString().Trim('-');
return result.Length == 0 ? "item" : result;
}
private static string Normalize(string text)
{
return text.Trim().ToLowerInvariant().Replace("_", "-").Replace(" ", "-").Replace("'", "");
}
}
@@ -11,6 +11,9 @@ public static partial class ZeroSpaceLinks
private static readonly IReadOnlyDictionary<string, string> MasteryUrls = SystemMasteries.All private static readonly IReadOnlyDictionary<string, string> MasteryUrls = SystemMasteries.All
.ToDictionary(static mastery => mastery.Name, static mastery => $"/SystemMastery/{mastery.Id}", StringComparer.OrdinalIgnoreCase); .ToDictionary(static mastery => mastery.Name, static mastery => $"/SystemMastery/{mastery.Id}", StringComparer.OrdinalIgnoreCase);
private static readonly IReadOnlyDictionary<string, string> ReferenceUrls = ReferenceCatalog.All
.ToDictionary(static item => item.Name, static item => item.CanonicalUrl, StringComparer.OrdinalIgnoreCase);
public static string? UrlFor(string name) public static string? UrlFor(string name)
{ {
if (SystemUrls.TryGetValue(name, out var systemUrl)) if (SystemUrls.TryGetValue(name, out var systemUrl))
@@ -18,7 +21,12 @@ public static partial class ZeroSpaceLinks
return systemUrl; return systemUrl;
} }
return MasteryUrls.TryGetValue(name, out var masteryUrl) ? masteryUrl : null; if (MasteryUrls.TryGetValue(name, out var masteryUrl))
{
return masteryUrl;
}
return ReferenceUrls.TryGetValue(name, out var referenceUrl) ? referenceUrl : null;
} }
public static string ResolveWikiLinks(string markdown) public static string ResolveWikiLinks(string markdown)
+3
View File
@@ -32,6 +32,9 @@ public static class SiteMap
new(typeof(Pages.Loadout), "Loadout", "loadout", "bi bi-items-nav-menu", null, new(typeof(Pages.Loadout), "Loadout", "loadout", "bi bi-items-nav-menu", null,
NavLinkMatch.All, Released: true, ShowInNav: true), NavLinkMatch.All, Released: true, ShowInNav: true),
new(typeof(Pages.ReferenceDoc), "Reference", "reference", "bi bi-reference-nav-menu", null,
NavLinkMatch.Prefix, Released: true, ShowInNav: true),
// Detail dossiers: part of the galaxy experience (linked from the map), // Detail dossiers: part of the galaxy experience (linked from the map),
// but not top-level navigation destinations. // but not top-level navigation destinations.
new(typeof(Pages.StarSystemDetail), "System Dossier", "StarSystem", "bi bi-star-systems-nav-menu", null, new(typeof(Pages.StarSystemDetail), "System Dossier", "StarSystem", "bi bi-star-systems-nav-menu", null,
Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.1 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.6 KiB

After

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.9 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.5 KiB

After

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 9.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.4 KiB

After

Width:  |  Height:  |  Size: 8.6 KiB

+14
View File
@@ -0,0 +1,14 @@
{
"nodes":[
{"id":"99ea1b2bbf92777c","type":"text","text":"---\n\n---\n# Bonuses\nTabs:\n- Occupation Bonus\n- Planetary Mastery Levels","x":-100,"y":-160,"width":396,"height":328},
{"id":"29c6f20df6d3d44a","x":-700,"y":-184,"width":396,"height":384,"type":"text","text":"---\n\n---\n# Co-Op Information DB\n- Commanders\n\t- Aster\n\t- etc.\n- Maps\n\t- Among the Remants\n\t- etc.\n- Mechanics\n\t- Items\n\t- Bonuses\n\t- Modifiers"},
{"id":"fa0ed6154212f81d","type":"text","text":"---\n\n---\n# Modifiers\nTabs:\n- Mutator\n- Boon","x":-100,"y":168,"width":396,"height":328},
{"id":"497699bececf0465","type":"text","text":"---\n\n---\n# Items\nTabs:\n- Faction Item Powers\n- General Items\n- Planetary Mastery Items","x":-100,"y":-488,"width":396,"height":328},
{"id":"2193554fea8653b3","x":-940,"y":600,"width":760,"height":420,"type":"text","text":"export abstract class MercFaction extends Faction {\n  override get subtype(): FactionType {\n    return \"mercenary\";\n  }\n\n  get factionType(): FactionType {\n    return this.subtype;\n  }\n}"}
],
"edges":[
{"id":"35b773c0445e0b38","fromNode":"29c6f20df6d3d44a","fromSide":"right","toNode":"497699bececf0465","toSide":"left"},
{"id":"a357c58962c6f662","fromNode":"29c6f20df6d3d44a","fromSide":"right","toNode":"99ea1b2bbf92777c","toSide":"left"},
{"id":"63dc332c6757eef4","fromNode":"29c6f20df6d3d44a","fromSide":"right","toNode":"fa0ed6154212f81d","toSide":"left"}
]
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 962 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 962 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 928 KiB