Vibe json data export
This commit is contained in:
@@ -0,0 +1,19 @@
|
||||
namespace ZeroSpace.Model;
|
||||
|
||||
public sealed record GalaxyMapExportData(
|
||||
DateTimeOffset ExportedAt,
|
||||
int TotalSystems,
|
||||
int TotalMasteries,
|
||||
IReadOnlyList<StarSystemData> Systems,
|
||||
IReadOnlyList<SystemMasteryData> Masteries,
|
||||
IReadOnlyList<PlanetaryMasteryItemData> PlanetaryMasteryItems)
|
||||
{
|
||||
public static GalaxyMapExportData CreateDefault() =>
|
||||
new(
|
||||
DateTimeOffset.UtcNow,
|
||||
StarSystems.All.Count,
|
||||
SystemMasteries.All.Count,
|
||||
StarSystems.All,
|
||||
SystemMasteries.All,
|
||||
ZeroSpace.Model.PlanetaryMasteryItems.All);
|
||||
}
|
||||
@@ -2,6 +2,8 @@
|
||||
@using System.Globalization
|
||||
@using System.Text.RegularExpressions
|
||||
@inject NavigationManager Navigation
|
||||
@inject IJSRuntime Js
|
||||
@inject IGalaxyMapExportSerializer ExportSerializer
|
||||
|
||||
<PageTitle>Galaxy Map</PageTitle>
|
||||
|
||||
@@ -9,7 +11,8 @@
|
||||
<div class="d-flex flex-wrap align-items-center justify-content-between mb-3 gap-3">
|
||||
<div>
|
||||
<h1 class="h2 mb-1 text-light fw-bold">Galaxy Map</h1>
|
||||
<p class="text-secondary mb-0 small">Interactive tactical star chart displaying all planetary systems and hyperlane routes.</p>
|
||||
<p class="text-secondary mb-0 small">Interactive tactical star chart displaying all planetary systems and
|
||||
hyperlane routes.</p>
|
||||
</div>
|
||||
<div class="d-flex flex-wrap align-items-center gap-3">
|
||||
<div class="map-legend d-flex flex-wrap align-items-center gap-3 px-3 py-2 rounded">
|
||||
@@ -34,6 +37,19 @@
|
||||
<span class="mx-1">•</span>
|
||||
<span><strong>@_connections.Count</strong> Hyperlanes</span>
|
||||
</div>
|
||||
<button class="btn btn-sm btn-scifi-outline d-inline-flex align-items-center gap-2 px-3 py-2"
|
||||
@onclick="DownloadGalaxyJsonAsync"
|
||||
disabled="@_isExporting"
|
||||
title="Download galaxy map data as JSON">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" viewBox="0 0 16 16"
|
||||
aria-hidden="true">
|
||||
<path
|
||||
d="M.5 9.9a.5.5 0 0 1 .5.5v2.5a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-2.5a.5.5 0 0 1 1 0v2.5a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2v-2.5a.5.5 0 0 1 .5-.5z"/>
|
||||
<path
|
||||
d="M7.646 11.854a.5.5 0 0 0 .708 0l3-3a.5.5 0 0 0-.708-.708L8.5 10.293V1.5a.5.5 0 0 0-1 0v8.793L5.354 8.146a.5.5 0 1 0-.708.708l3 3z"/>
|
||||
</svg>
|
||||
<span>@(_isExporting ? "Exporting..." : "Download JSON")</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -41,7 +57,8 @@
|
||||
<div class="filter-bar zs-glass-card p-3 mb-3 d-flex flex-wrap align-items-center gap-3">
|
||||
<div class="d-flex align-items-center gap-2 filter-item">
|
||||
<label class="filter-label text-dim small fw-semibold text-uppercase">System Type:</label>
|
||||
<select class="form-select form-select-sm scifi-select" @bind="_selectedCategory" @bind:after="ApplyFilters">
|
||||
<select class="form-select form-select-sm scifi-select" @bind="_selectedCategory"
|
||||
@bind:after="ApplyFilters">
|
||||
<option value="">All Types</option>
|
||||
<option value="@StarSystemCategory.HomeworldSystem">Homeworld System</option>
|
||||
<option value="@StarSystemCategory.CoreSystem">Core System</option>
|
||||
@@ -69,7 +86,7 @@
|
||||
<option value="Influence">Higher Influence</option>
|
||||
<option value="Credits">Higher Credits</option>
|
||||
<option value="Energy">Higher Energy</option>
|
||||
<option value="Items">Higher Items</option>
|
||||
<option value="Items">Higher Items (In current item, items only attainable in shops)</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
@@ -83,263 +100,272 @@
|
||||
|
||||
<div class="galaxy-layout">
|
||||
<div class="galaxy-map-container" @onmouseleave="OnContainerMouseLeave">
|
||||
<svg viewBox="0 0 1000 1000" class="galaxy-svg" preserveAspectRatio="xMidYMid meet"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<!-- Background Grid Pattern -->
|
||||
<pattern id="space-grid" width="50" height="50" patternUnits="userSpaceOnUse">
|
||||
<path d="M 50 0 L 0 0 0 50" fill="none" stroke="rgba(56, 189, 248, 0.04)" stroke-width="1"/>
|
||||
</pattern>
|
||||
<svg viewBox="0 0 1000 1000" class="galaxy-svg" preserveAspectRatio="xMidYMid meet"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<!-- Background Grid Pattern -->
|
||||
<pattern id="space-grid" width="50" height="50" patternUnits="userSpaceOnUse">
|
||||
<path d="M 50 0 L 0 0 0 50" fill="none" stroke="rgba(56, 189, 248, 0.04)" stroke-width="1"/>
|
||||
</pattern>
|
||||
|
||||
<!-- Center Core Glow -->
|
||||
<radialGradient id="galaxy-core-glow" cx="50%" cy="50%" r="50%">
|
||||
<stop offset="0%" stop-color="rgba(56, 189, 248, 0.08)"/>
|
||||
<stop offset="60%" stop-color="rgba(16, 185, 129, 0.03)"/>
|
||||
<stop offset="100%" stop-color="rgba(13, 17, 23, 0)"/>
|
||||
</radialGradient>
|
||||
<!-- Center Core Glow -->
|
||||
<radialGradient id="galaxy-core-glow" cx="50%" cy="50%" r="50%">
|
||||
<stop offset="0%" stop-color="rgba(56, 189, 248, 0.08)"/>
|
||||
<stop offset="60%" stop-color="rgba(16, 185, 129, 0.03)"/>
|
||||
<stop offset="100%" stop-color="rgba(13, 17, 23, 0)"/>
|
||||
</radialGradient>
|
||||
|
||||
<!-- Glow filters for Owners / Factions -->
|
||||
<filter id="glow-eden" x="-50%" y="-50%" width="200%" height="200%">
|
||||
<feGaussianBlur in="SourceGraphic" stdDeviation="3" result="blur"/>
|
||||
<feMerge>
|
||||
<feMergeNode in="blur"/>
|
||||
<feMergeNode in="SourceGraphic"/>
|
||||
</feMerge>
|
||||
</filter>
|
||||
<filter id="glow-keeper" x="-50%" y="-50%" width="200%" height="200%">
|
||||
<feGaussianBlur in="SourceGraphic" stdDeviation="3" result="blur"/>
|
||||
<feMerge>
|
||||
<feMergeNode in="blur"/>
|
||||
<feMergeNode in="SourceGraphic"/>
|
||||
</feMerge>
|
||||
</filter>
|
||||
<filter id="glow-obsidian" x="-50%" y="-50%" width="200%" height="200%">
|
||||
<feGaussianBlur in="SourceGraphic" stdDeviation="3" result="blur"/>
|
||||
<feMerge>
|
||||
<feMergeNode in="blur"/>
|
||||
<feMergeNode in="SourceGraphic"/>
|
||||
</feMerge>
|
||||
</filter>
|
||||
</defs>
|
||||
<!-- Glow filters for Owners / Factions -->
|
||||
<filter id="glow-eden" x="-50%" y="-50%" width="200%" height="200%">
|
||||
<feGaussianBlur in="SourceGraphic" stdDeviation="3" result="blur"/>
|
||||
<feMerge>
|
||||
<feMergeNode in="blur"/>
|
||||
<feMergeNode in="SourceGraphic"/>
|
||||
</feMerge>
|
||||
</filter>
|
||||
<filter id="glow-keeper" x="-50%" y="-50%" width="200%" height="200%">
|
||||
<feGaussianBlur in="SourceGraphic" stdDeviation="3" result="blur"/>
|
||||
<feMerge>
|
||||
<feMergeNode in="blur"/>
|
||||
<feMergeNode in="SourceGraphic"/>
|
||||
</feMerge>
|
||||
</filter>
|
||||
<filter id="glow-obsidian" x="-50%" y="-50%" width="200%" height="200%">
|
||||
<feGaussianBlur in="SourceGraphic" stdDeviation="3" result="blur"/>
|
||||
<feMerge>
|
||||
<feMergeNode in="blur"/>
|
||||
<feMergeNode in="SourceGraphic"/>
|
||||
</feMerge>
|
||||
</filter>
|
||||
</defs>
|
||||
|
||||
<!-- Map Background & Grid -->
|
||||
<rect width="1000" height="1000" class="map-bg"/>
|
||||
<rect width="1000" height="1000" fill="url(#space-grid)"/>
|
||||
<rect width="1000" height="1000" fill="url(#galaxy-core-glow)"/>
|
||||
<!-- Map Background & Grid -->
|
||||
<rect width="1000" height="1000" class="map-bg"/>
|
||||
<rect width="1000" height="1000" fill="url(#space-grid)"/>
|
||||
<rect width="1000" height="1000" fill="url(#galaxy-core-glow)"/>
|
||||
|
||||
<!-- Connection Lines (Hyperlanes) -->
|
||||
<g class="hyperlanes">
|
||||
@foreach (var edge in _connections)
|
||||
{
|
||||
var isFromMatch = _matchingSystemIds.Contains(edge.From.Id);
|
||||
var isToMatch = _matchingSystemIds.Contains(edge.To.Id);
|
||||
var isFilterDimmed = HasActiveFilters && (!isFromMatch || !isToMatch);
|
||||
<!-- Connection Lines (Hyperlanes) -->
|
||||
<g class="hyperlanes">
|
||||
@foreach (var edge in _connections)
|
||||
{
|
||||
var isFromMatch = _matchingSystemIds.Contains(edge.From.Id);
|
||||
var isToMatch = _matchingSystemIds.Contains(edge.To.Id);
|
||||
var isFilterDimmed = HasActiveFilters && (!isFromMatch || !isToMatch);
|
||||
|
||||
var isHoverHighlighted = _hoveredSystem is not null &&
|
||||
(string.Equals(edge.From.Id, _hoveredSystem.Id, StringComparison.OrdinalIgnoreCase) ||
|
||||
string.Equals(edge.To.Id, _hoveredSystem.Id, StringComparison.OrdinalIgnoreCase));
|
||||
var isHoverDimmed = _hoveredSystem is not null && !isHoverHighlighted;
|
||||
var isHoverHighlighted = _hoveredSystem is not null &&
|
||||
(string.Equals(edge.From.Id, _hoveredSystem.Id, StringComparison.OrdinalIgnoreCase) ||
|
||||
string.Equals(edge.To.Id, _hoveredSystem.Id, StringComparison.OrdinalIgnoreCase));
|
||||
var isHoverDimmed = _hoveredSystem is not null && !isHoverHighlighted;
|
||||
|
||||
var lineClass = isHoverHighlighted ? "hyperlane-line hyperlane-highlighted" :
|
||||
(isHoverDimmed || isFilterDimmed) ? "hyperlane-line hyperlane-dimmed" : "hyperlane-line";
|
||||
<line x1="@SvgNumber(edge.From.X)"
|
||||
y1="@SvgNumber(edge.From.Y)"
|
||||
x2="@SvgNumber(edge.To.X)"
|
||||
y2="@SvgNumber(edge.To.Y)"
|
||||
class="@lineClass"/>
|
||||
}
|
||||
</g>
|
||||
var lineClass = isHoverHighlighted ? "hyperlane-line hyperlane-highlighted" :
|
||||
(isHoverDimmed || isFilterDimmed) ? "hyperlane-line hyperlane-dimmed" : "hyperlane-line";
|
||||
<line x1="@SvgNumber(edge.From.X)"
|
||||
y1="@SvgNumber(edge.From.Y)"
|
||||
x2="@SvgNumber(edge.To.X)"
|
||||
y2="@SvgNumber(edge.To.Y)"
|
||||
class="@lineClass"/>
|
||||
}
|
||||
</g>
|
||||
|
||||
<!-- Planetary Nodes -->
|
||||
<g class="planetary-nodes">
|
||||
@foreach (var system in _systems)
|
||||
{
|
||||
var ownerClass = GetOwnerCssClass(system.Owner);
|
||||
var radius = GetNodeRadius(system.SystemCategory);
|
||||
var isMatch = _matchingSystemIds.Contains(system.Id);
|
||||
var isHovered = _hoveredSystem is not null && string.Equals(system.Id, _hoveredSystem.Id, StringComparison.OrdinalIgnoreCase);
|
||||
var isNeighbor = _hoveredConnectedIds.Contains(system.Id);
|
||||
var isDimmed = (_hoveredSystem is not null && !isHovered && !isNeighbor) || (HasActiveFilters && !isMatch);
|
||||
var stateClass = isHovered ? "is-hovered" :
|
||||
isNeighbor ? "is-neighbor" :
|
||||
isDimmed ? "is-dimmed" :
|
||||
(HasActiveFilters && isMatch ? "is-filter-matched" : "");
|
||||
<!-- Planetary Nodes -->
|
||||
<g class="planetary-nodes">
|
||||
@foreach (var system in _systems)
|
||||
{
|
||||
var ownerClass = GetOwnerCssClass(system.Owner);
|
||||
var radius = GetNodeRadius(system.SystemCategory);
|
||||
var isMatch = _matchingSystemIds.Contains(system.Id);
|
||||
var isHovered = _hoveredSystem is not null && string.Equals(system.Id, _hoveredSystem.Id, StringComparison.OrdinalIgnoreCase);
|
||||
var isNeighbor = _hoveredConnectedIds.Contains(system.Id);
|
||||
var isDimmed = (_hoveredSystem is not null && !isHovered && !isNeighbor) || (HasActiveFilters && !isMatch);
|
||||
var stateClass = isHovered ? "is-hovered" :
|
||||
isNeighbor ? "is-neighbor" :
|
||||
isDimmed ? "is-dimmed" :
|
||||
(HasActiveFilters && isMatch ? "is-filter-matched" : "");
|
||||
|
||||
<g class="node-group @ownerClass @stateClass"
|
||||
transform="translate(@SvgNumber(system.X), @SvgNumber(system.Y))"
|
||||
@onclick="() => OnNodeClick(system.Id)"
|
||||
@onmouseenter="() => OnNodeMouseEnter(system)"
|
||||
@onmouseleave="() => OnNodeMouseLeave(system)"
|
||||
@onfocus="() => OnNodeFocus(system)"
|
||||
@onblur="OnNodeBlur"
|
||||
@onkeydown="e => OnNodeKeyDown(e, system.Id)"
|
||||
tabindex="0"
|
||||
role="button"
|
||||
aria-label="@system.Name (@system.SystemCategory.ToDisplayName(), @system.Owner)">
|
||||
<g class="node-group @ownerClass @stateClass"
|
||||
transform="translate(@SvgNumber(system.X), @SvgNumber(system.Y))"
|
||||
@onclick="() => OnNodeClick(system.Id)"
|
||||
@onmouseenter="() => OnNodeMouseEnter(system)"
|
||||
@onmouseleave="() => OnNodeMouseLeave(system)"
|
||||
@onfocus="() => OnNodeFocus(system)"
|
||||
@onblur="OnNodeBlur"
|
||||
@onkeydown="e => OnNodeKeyDown(e, system.Id)"
|
||||
tabindex="0"
|
||||
role="button"
|
||||
aria-label="@system.Name (@system.SystemCategory.ToDisplayName(), @system.Owner)">
|
||||
|
||||
<!-- Outer aura/glow circle -->
|
||||
<circle r="@SvgNumber(radius + 4)" class="node-aura"/>
|
||||
<!-- Outer aura/glow circle -->
|
||||
<circle r="@SvgNumber(radius + 4)" class="node-aura"/>
|
||||
|
||||
<!-- Main Node Circle -->
|
||||
<circle r="@SvgNumber(radius)" class="node-circle"/>
|
||||
<!-- Main Node Circle -->
|
||||
<circle r="@SvgNumber(radius)" class="node-circle"/>
|
||||
|
||||
<!-- Center core dot for homeworlds and core systems -->
|
||||
@if (system.SystemCategory != StarSystemCategory.SideSystem)
|
||||
{
|
||||
<circle r="@SvgNumber(radius * 0.4)" class="node-core-dot"/>
|
||||
}
|
||||
<!-- Center core dot for homeworlds and core systems -->
|
||||
@if (system.SystemCategory != StarSystemCategory.SideSystem)
|
||||
{
|
||||
<circle r="@SvgNumber(radius * 0.4)" class="node-core-dot"/>
|
||||
}
|
||||
|
||||
<!-- System Name Label -->
|
||||
<text y="@SvgNumber(radius + 10)" class="node-label" text-anchor="middle">@system.Name</text>
|
||||
</g>
|
||||
}
|
||||
</g>
|
||||
</svg>
|
||||
<!-- System Name Label -->
|
||||
<text y="@SvgNumber(radius + 10)" class="node-label"
|
||||
text-anchor="middle">@system.Name</text>
|
||||
</g>
|
||||
}
|
||||
</g>
|
||||
</svg>
|
||||
|
||||
<!-- Floating Glassmorphism Hover Card -->
|
||||
@if (_hoveredSystem is not null)
|
||||
{
|
||||
var ownerClass = GetOwnerCssClass(_hoveredSystem.Owner);
|
||||
var higherYields = GetHigherYields(_hoveredSystem);
|
||||
<div class="planet-hover-card @ownerClass"
|
||||
style="@GetTooltipStyle(_hoveredSystem)"
|
||||
role="tooltip"
|
||||
aria-live="polite">
|
||||
<div class="card-header-row d-flex align-items-center justify-content-between mb-2">
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<span class="system-owner-indicator"></span>
|
||||
<span class="system-title fw-bold text-light">@_hoveredSystem.Name</span>
|
||||
</div>
|
||||
<span class="badge @CategoryBadgeClass(_hoveredSystem.SystemCategory)">@_hoveredSystem.SystemCategory.ToDisplayName()</span>
|
||||
</div>
|
||||
|
||||
<div class="card-details-grid">
|
||||
<div class="detail-row">
|
||||
<span class="detail-label">Faction Owner</span>
|
||||
<span class="badge @OwnerBadgeClass(_hoveredSystem.Owner) mt-1">@_hoveredSystem.Owner</span>
|
||||
<!-- Floating Glassmorphism Hover Card -->
|
||||
@if (_hoveredSystem is not null)
|
||||
{
|
||||
var ownerClass = GetOwnerCssClass(_hoveredSystem.Owner);
|
||||
var higherYields = GetHigherYields(_hoveredSystem);
|
||||
<div class="planet-hover-card @ownerClass"
|
||||
style="@GetTooltipStyle(_hoveredSystem)"
|
||||
role="tooltip"
|
||||
aria-live="polite">
|
||||
<div class="card-header-row d-flex align-items-center justify-content-between mb-2">
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<span class="system-owner-indicator"></span>
|
||||
<span class="system-title fw-bold text-light">@_hoveredSystem.Name</span>
|
||||
</div>
|
||||
<span
|
||||
class="badge @CategoryBadgeClass(_hoveredSystem.SystemCategory)">@_hoveredSystem.SystemCategory.ToDisplayName()</span>
|
||||
</div>
|
||||
|
||||
<div class="detail-row">
|
||||
<span class="detail-label">Occupation Buff</span>
|
||||
<span class="detail-value @(_hoveredSystem.OccupationBuff != null ? "text-buff" : "text-secondary")">
|
||||
<div class="card-details-grid">
|
||||
<div class="detail-row">
|
||||
<span class="detail-label">Faction Owner</span>
|
||||
<span class="badge @OwnerBadgeClass(_hoveredSystem.Owner) mt-1">@_hoveredSystem.Owner</span>
|
||||
</div>
|
||||
|
||||
<div class="detail-row">
|
||||
<span class="detail-label">Occupation Buff</span>
|
||||
<span
|
||||
class="detail-value @(_hoveredSystem.OccupationBuff != null ? "text-buff" : "text-secondary")">
|
||||
@(_hoveredSystem.OccupationBuff ?? "—")
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="detail-row">
|
||||
<span class="detail-label">System Mastery</span>
|
||||
<span class="detail-value @(_hoveredSystem.SystemMastery != null ? "text-mastery" : "text-secondary")">
|
||||
<div class="detail-row">
|
||||
<span class="detail-label">System Mastery</span>
|
||||
<span
|
||||
class="detail-value @(_hoveredSystem.SystemMastery != null ? "text-mastery" : "text-secondary")">
|
||||
@(_hoveredSystem.SystemMastery ?? "—")
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="detail-row">
|
||||
<span class="detail-label">Yield Bonuses</span>
|
||||
<div class="yield-badges d-flex flex-wrap gap-1 mt-1">
|
||||
@if (higherYields.Count == 0)
|
||||
{
|
||||
<span class="yield-badge yield-standard">Standard</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
@foreach (var y in higherYields)
|
||||
<div class="detail-row">
|
||||
<span class="detail-label">Yield Bonuses</span>
|
||||
<div class="yield-badges d-flex flex-wrap gap-1 mt-1">
|
||||
@if (higherYields.Count == 0)
|
||||
{
|
||||
<span class="yield-badge yield-@y.ToLowerInvariant()">+@y</span>
|
||||
<span class="yield-badge yield-standard">Standard</span>
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
@foreach (var y in higherYields)
|
||||
{
|
||||
<span class="yield-badge yield-@y.ToLowerInvariant()">+@y</span>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="detail-row">
|
||||
<div class="d-flex align-items-center justify-content-between">
|
||||
<span class="detail-label">Connected Systems</span>
|
||||
<span
|
||||
class="badge bg-dark text-secondary border border-secondary border-opacity-25">@_hoveredSystem.ConnectedSystems.Count</span>
|
||||
</div>
|
||||
<div class="connected-systems-list text-secondary small mt-1">
|
||||
@string.Join(" • ", _hoveredSystem.ConnectedSystems)
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="detail-row">
|
||||
<div class="d-flex align-items-center justify-content-between">
|
||||
<span class="detail-label">Connected Systems</span>
|
||||
<span class="badge bg-dark text-secondary border border-secondary border-opacity-25">@_hoveredSystem.ConnectedSystems.Count</span>
|
||||
</div>
|
||||
<div class="connected-systems-list text-secondary small mt-1">
|
||||
@string.Join(" • ", _hoveredSystem.ConnectedSystems)
|
||||
</div>
|
||||
<div class="card-footer-hint mt-2 pt-2 border-top border-secondary border-opacity-25 text-center">
|
||||
<span class="hint-text small">Click system to view full details →</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-footer-hint mt-2 pt-2 border-top border-secondary border-opacity-25 text-center">
|
||||
<span class="hint-text small">Click system to view full details →</span>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<aside class="faction-drawer zs-glass-card @(_drawerCollapsed ? "is-collapsed" : "")" aria-label="Faction occupational bonuses">
|
||||
<div class="drawer-header d-flex align-items-center justify-content-between gap-2">
|
||||
<div>
|
||||
<h2 class="drawer-title text-light fw-bold mb-0">Faction Bonuses</h2>
|
||||
<p class="drawer-subtitle text-secondary mb-0">Occupation buffs by faction</p>
|
||||
</div>
|
||||
<button class="btn btn-sm btn-scifi-outline drawer-toggle" @onclick="ToggleDrawer"
|
||||
aria-expanded="@(!_drawerCollapsed)"
|
||||
title="@(_drawerCollapsed ? "Expand bonus panel" : "Collapse bonus panel")">
|
||||
@(_drawerCollapsed ? "»" : "«")
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
|
||||
@if (!_drawerCollapsed)
|
||||
{
|
||||
<div class="drawer-body">
|
||||
<div class="faction-picker" role="radiogroup" aria-label="Select faction">
|
||||
@foreach (var owner in StarSystemOwners.All)
|
||||
{
|
||||
var isActive = string.Equals(owner, _selectedFaction, StringComparison.Ordinal);
|
||||
<button role="radio" aria-checked="@isActive"
|
||||
class="faction-pick @(isActive ? "is-active " + GetOwnerCssClass(owner) : "")"
|
||||
@onclick="() => SelectFaction(owner)">
|
||||
<span class="legend-dot @FactionDotClass(owner)"></span>
|
||||
<span class="faction-pick-name">@ShortFactionName(owner)</span>
|
||||
<span class="faction-pick-count">@FactionSystemCount(owner)</span>
|
||||
</button>
|
||||
}
|
||||
<aside class="faction-drawer zs-glass-card @(_drawerCollapsed ? "is-collapsed" : "")"
|
||||
aria-label="Faction occupational bonuses">
|
||||
<div class="drawer-header d-flex align-items-center justify-content-between gap-2">
|
||||
<div>
|
||||
<h2 class="drawer-title text-light fw-bold mb-0">Faction Bonuses</h2>
|
||||
<p class="drawer-subtitle text-secondary mb-0">Occupation buffs by faction</p>
|
||||
</div>
|
||||
<button class="btn btn-sm btn-scifi-outline drawer-toggle" @onclick="ToggleDrawer"
|
||||
aria-expanded="@(!_drawerCollapsed)"
|
||||
title="@(_drawerCollapsed ? "Expand bonus panel" : "Collapse bonus panel")">
|
||||
@(_drawerCollapsed ? "»" : "«")
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="drawer-section">
|
||||
<h3 class="drawer-section-title">Occupational bonuses</h3>
|
||||
<p class="drawer-summary text-secondary">
|
||||
<strong class="text-light">@FactionSystems.Count</strong> systems owned by
|
||||
<strong class="text-light">@ShortFactionName(_selectedFaction)</strong>
|
||||
</p>
|
||||
@if (FactionBonuses.Count == 0)
|
||||
{
|
||||
<p class="text-secondary small mb-0">No occupation buffs recorded for this faction's systems.</p>
|
||||
}
|
||||
else
|
||||
{
|
||||
<ul class="bonus-list list-unstyled mb-0">
|
||||
@foreach (var bonus in FactionBonuses.OrderBy(b => b.Label))
|
||||
@if (!_drawerCollapsed)
|
||||
{
|
||||
<div class="drawer-body">
|
||||
<div class="faction-picker" role="radiogroup" aria-label="Select faction">
|
||||
@foreach (var owner in StarSystemOwners.All)
|
||||
{
|
||||
var isActive = string.Equals(owner, _selectedFaction, StringComparison.Ordinal);
|
||||
<button role="radio" aria-checked="@isActive"
|
||||
class="faction-pick @(isActive ? "is-active " + GetOwnerCssClass(owner) : "")"
|
||||
@onclick="() => SelectFaction(owner)">
|
||||
<span class="legend-dot @FactionDotClass(owner)"></span>
|
||||
<span class="faction-pick-name">@ShortFactionName(owner)</span>
|
||||
<span class="faction-pick-count">@FactionSystemCount(owner)</span>
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="drawer-section">
|
||||
<h3 class="drawer-section-title">Occupational bonuses</h3>
|
||||
<p class="drawer-summary text-secondary">
|
||||
<strong class="text-light">@FactionSystems.Count</strong> systems owned by
|
||||
<strong class="text-light">@ShortFactionName(_selectedFaction)</strong>
|
||||
</p>
|
||||
@if (FactionBonuses.Count == 0)
|
||||
{
|
||||
<p class="text-secondary small mb-0">No occupation buffs recorded for this faction's
|
||||
systems.</p>
|
||||
}
|
||||
else
|
||||
{
|
||||
<ul class="bonus-list list-unstyled mb-0">
|
||||
@foreach (var bonus in FactionBonuses.OrderBy(b => b.Label))
|
||||
{
|
||||
<li class="bonus-row">
|
||||
<span class="bonus-total">@FormatBonusTotal(bonus)</span>
|
||||
<span class="bonus-label">@bonus.Label</span>
|
||||
<span
|
||||
class="bonus-count">@bonus.PlanetCount @(bonus.PlanetCount == 1 ? "planet" : "planets")</span>
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="drawer-section">
|
||||
<h3 class="drawer-section-title">Planets (@FactionSystems.Count)</h3>
|
||||
<ul class="planet-list list-unstyled mb-0">
|
||||
@foreach (var system in FactionSystems)
|
||||
{
|
||||
<li class="bonus-row">
|
||||
<span class="bonus-total">@FormatBonusTotal(bonus)</span>
|
||||
<span class="bonus-label">@bonus.Label</span>
|
||||
<span class="bonus-count">@bonus.PlanetCount @(bonus.PlanetCount == 1 ? "planet" : "planets")</span>
|
||||
<li>
|
||||
<button class="planet-row" @onclick="() => OnNodeClick(system.Id)"
|
||||
title="View @system.Name details">
|
||||
<span class="planet-name">@system.Name</span>
|
||||
<span class="planet-buff">@(system.OccupationBuff ?? "—")</span>
|
||||
</button>
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="drawer-section">
|
||||
<h3 class="drawer-section-title">Planets (@FactionSystems.Count)</h3>
|
||||
<ul class="planet-list list-unstyled mb-0">
|
||||
@foreach (var system in FactionSystems)
|
||||
{
|
||||
<li>
|
||||
<button class="planet-row" @onclick="() => OnNodeClick(system.Id)" title="View @system.Name details">
|
||||
<span class="planet-name">@system.Name</span>
|
||||
<span class="planet-buff">@(system.OccupationBuff ?? "—")</span>
|
||||
</button>
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</aside>
|
||||
}
|
||||
</aside>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -362,6 +388,7 @@
|
||||
// Faction picker: one of the three factions is always selected.
|
||||
private string _selectedFaction = StarSystemOwners.EdenAccord;
|
||||
private bool _drawerCollapsed;
|
||||
private bool _isExporting;
|
||||
|
||||
private bool HasActiveFilters =>
|
||||
_selectedCategory.HasValue ||
|
||||
@@ -386,6 +413,7 @@
|
||||
_matchingSystemIds.Add(system.Id);
|
||||
}
|
||||
}
|
||||
|
||||
_matchingCount = _matchingSystemIds.Count;
|
||||
}
|
||||
|
||||
@@ -434,6 +462,23 @@
|
||||
ApplyFilters();
|
||||
}
|
||||
|
||||
private async Task DownloadGalaxyJsonAsync()
|
||||
{
|
||||
if (_isExporting) return;
|
||||
|
||||
try
|
||||
{
|
||||
_isExporting = true;
|
||||
var exportData = GalaxyMapExportData.CreateDefault();
|
||||
var json = ExportSerializer.Serialize(exportData);
|
||||
await Js.InvokeVoidAsync("downloadFileFromText", "galaxy-map-data.json", "application/json", json);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_isExporting = false;
|
||||
}
|
||||
}
|
||||
|
||||
// ----- faction bonus drawer -----
|
||||
|
||||
public sealed record FactionBonus(string Label, bool IsPercent, double Total, int PlanetCount);
|
||||
|
||||
@@ -12,9 +12,9 @@ builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.
|
||||
builder.Services.AddHttpClient<IUnitsApiClient, UnitsApiClient>(client =>
|
||||
{
|
||||
var apiBaseUrl = builder.Configuration["services:apiservice:https:0"]
|
||||
?? builder.Configuration["services:apiservice:http:0"]
|
||||
?? builder.Configuration["ApiServiceUrl"]
|
||||
?? builder.HostEnvironment.BaseAddress;
|
||||
?? builder.Configuration["services:apiservice:http:0"]
|
||||
?? builder.Configuration["ApiServiceUrl"]
|
||||
?? builder.HostEnvironment.BaseAddress;
|
||||
|
||||
if (!apiBaseUrl.EndsWith('/'))
|
||||
{
|
||||
@@ -24,4 +24,6 @@ builder.Services.AddHttpClient<IUnitsApiClient, UnitsApiClient>(client =>
|
||||
client.BaseAddress = new Uri(apiBaseUrl);
|
||||
});
|
||||
|
||||
builder.Services.AddSingleton<IGalaxyMapExportSerializer, GalaxyMapExportSerializer>();
|
||||
|
||||
await builder.Build().RunAsync();
|
||||
@@ -0,0 +1,28 @@
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using ZeroSpace.Model;
|
||||
|
||||
namespace ZeroSpace.Web.Services;
|
||||
|
||||
public sealed class GalaxyMapExportSerializer : IGalaxyMapExportSerializer
|
||||
{
|
||||
public static readonly JsonSerializerOptions DefaultOptions = CreateDefaultOptions();
|
||||
|
||||
public static JsonSerializerOptions CreateDefaultOptions()
|
||||
{
|
||||
var options = new JsonSerializerOptions
|
||||
{
|
||||
WriteIndented = true,
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
|
||||
};
|
||||
options.Converters.Add(new JsonStringEnumConverter(JsonNamingPolicy.CamelCase));
|
||||
return options;
|
||||
}
|
||||
|
||||
public string Serialize(GalaxyMapExportData data) => SerializeData(data);
|
||||
|
||||
public static string SerializeData(GalaxyMapExportData data, JsonSerializerOptions? options = null)
|
||||
{
|
||||
return JsonSerializer.Serialize(data, options ?? DefaultOptions);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
using ZeroSpace.Model;
|
||||
|
||||
namespace ZeroSpace.Web.Services;
|
||||
|
||||
public interface IGalaxyMapExportSerializer
|
||||
{
|
||||
string Serialize(GalaxyMapExportData data);
|
||||
}
|
||||
@@ -29,6 +29,8 @@
|
||||
<a href="." class="reload">Reload</a>
|
||||
<span class="dismiss">🗙</span>
|
||||
</div>
|
||||
<script src="js/fileDownload.js"></script>
|
||||
|
||||
<script src="_framework/blazor.webassembly#[.{fingerprint}].js"></script>
|
||||
</body>
|
||||
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
window.downloadFileFromText = function (fileName, contentType, textContent) {
|
||||
const blob = new Blob([textContent], { type: contentType });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const anchor = document.createElement('a');
|
||||
anchor.href = url;
|
||||
anchor.download = fileName || 'download.json';
|
||||
document.body.appendChild(anchor);
|
||||
anchor.click();
|
||||
document.body.removeChild(anchor);
|
||||
URL.revokeObjectURL(url);
|
||||
};
|
||||
Reference in New Issue
Block a user