Adding quick vibe reference
@@ -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;
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 4.8 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 8.1 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 7.6 KiB After Width: | Height: | Size: 9.4 KiB |
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 8.9 KiB After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 8.5 KiB After Width: | Height: | Size: 7.6 KiB |
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 9.1 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 7.4 KiB After Width: | Height: | Size: 8.6 KiB |
@@ -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"}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 962 KiB |
|
After Width: | Height: | Size: 962 KiB |
|
After Width: | Height: | Size: 928 KiB |