- Removed the faction <select> filter from the Galaxy filter bar. - Added a right-side drawer; exactly one of the three factions is always selected (Eden Accord default), user picks via picker. - Drawer aggregates occupational bonuses across the selected faction's planets: buffs grouped by type with magnitudes summed (e.g. two +15% cooldown planets total +30%), plus planet counts. - Drawer also lists the faction's planets linking to detail pages, and collapses via the header toggle.
710 lines
29 KiB
Plaintext
710 lines
29 KiB
Plaintext
@page "/galaxy"
|
|
@using System.Globalization
|
|
@using System.Text.RegularExpressions
|
|
@inject NavigationManager Navigation
|
|
|
|
<PageTitle>Galaxy Map</PageTitle>
|
|
|
|
<div class="galaxy-page">
|
|
<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>
|
|
</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">
|
|
<div class="legend-group d-flex align-items-center gap-2">
|
|
<span class="legend-header text-dim fs-8 text-uppercase">Faction:</span>
|
|
<div class="legend-item d-flex align-items-center gap-1" title="The Eden Accord (Green)">
|
|
<span class="legend-dot dot-eden"></span>
|
|
<span class="legend-text">Eden Accord</span>
|
|
</div>
|
|
<div class="legend-item d-flex align-items-center gap-1" title="The Keeper Council (Blue)">
|
|
<span class="legend-dot dot-keeper"></span>
|
|
<span class="legend-text">Keeper Council</span>
|
|
</div>
|
|
<div class="legend-item d-flex align-items-center gap-1" title="The Obsidian Order (Red)">
|
|
<span class="legend-dot dot-obsidian"></span>
|
|
<span class="legend-text">Obsidian Order</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="map-stats text-secondary small px-3 py-2 rounded">
|
|
<span><strong>@_matchingCount</strong> / <strong>@_systems.Count</strong> Systems</span>
|
|
<span class="mx-1">•</span>
|
|
<span><strong>@_connections.Count</strong> Hyperlanes</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Filter Control Bar -->
|
|
<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">
|
|
<option value="">All Types</option>
|
|
<option value="@StarSystemCategory.HomeworldSystem">Homeworld System</option>
|
|
<option value="@StarSystemCategory.CoreSystem">Core System</option>
|
|
<option value="@StarSystemCategory.SideSystem">Side System</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div class="d-flex align-items-center gap-2 filter-item">
|
|
<label class="filter-label text-dim small fw-semibold text-uppercase">Mastery:</label>
|
|
<select class="form-select form-select-sm scifi-select" @bind="_selectedMastery" @bind:after="ApplyFilters">
|
|
<option value="">All Masteries</option>
|
|
<option value="__ANY__">Any Mastery</option>
|
|
@foreach (var mastery in _availableMasteries)
|
|
{
|
|
<option value="@mastery">@mastery</option>
|
|
}
|
|
</select>
|
|
</div>
|
|
|
|
<div class="d-flex align-items-center gap-2 filter-item">
|
|
<label class="filter-label text-dim small fw-semibold text-uppercase">Special Yields:</label>
|
|
<select class="form-select form-select-sm scifi-select" @bind="_selectedYield" @bind:after="ApplyFilters">
|
|
<option value="">All Yields</option>
|
|
<option value="ANY">Any Special Yield</option>
|
|
<option value="Influence">Higher Influence</option>
|
|
<option value="Credits">Higher Credits</option>
|
|
<option value="Energy">Higher Energy</option>
|
|
<option value="Items">Higher Items</option>
|
|
</select>
|
|
</div>
|
|
|
|
@if (HasActiveFilters)
|
|
{
|
|
<button class="btn btn-sm btn-scifi-outline py-1 px-3 ms-auto" @onclick="ResetFilters">
|
|
<span>Reset Filters</span>
|
|
</button>
|
|
}
|
|
</div>
|
|
|
|
<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>
|
|
|
|
<!-- 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>
|
|
|
|
<!-- 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);
|
|
|
|
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>
|
|
|
|
<!-- 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)">
|
|
|
|
<!-- Outer aura/glow circle -->
|
|
<circle r="@SvgNumber(radius + 4)" class="node-aura"/>
|
|
|
|
<!-- 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"/>
|
|
}
|
|
|
|
<!-- 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>
|
|
</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 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 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)
|
|
{
|
|
<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="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>
|
|
}
|
|
</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)
|
|
{
|
|
<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>
|
|
<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>
|
|
</div>
|
|
</div>
|
|
|
|
@code {
|
|
|
|
public sealed record ConnectionEdge(StarSystemData From, StarSystemData To);
|
|
|
|
private IReadOnlyList<StarSystemData> _systems = [];
|
|
private IReadOnlyList<ConnectionEdge> _connections = [];
|
|
private IReadOnlyList<string> _availableMasteries = [];
|
|
private StarSystemData? _hoveredSystem;
|
|
private readonly HashSet<string> _hoveredConnectedIds = new(StringComparer.OrdinalIgnoreCase);
|
|
private readonly HashSet<string> _matchingSystemIds = new(StringComparer.OrdinalIgnoreCase);
|
|
|
|
private StarSystemCategory? _selectedCategory;
|
|
private string _selectedMastery = string.Empty;
|
|
private string _selectedYield = string.Empty;
|
|
private int _matchingCount;
|
|
|
|
// Faction picker: one of the three factions is always selected.
|
|
private string _selectedFaction = StarSystemOwners.EdenAccord;
|
|
private bool _drawerCollapsed;
|
|
|
|
private bool HasActiveFilters =>
|
|
_selectedCategory.HasValue ||
|
|
!string.IsNullOrEmpty(_selectedMastery) ||
|
|
!string.IsNullOrEmpty(_selectedYield);
|
|
|
|
protected override void OnInitialized()
|
|
{
|
|
_systems = StarSystems.All;
|
|
_connections = BuildUniqueConnections(_systems);
|
|
_availableMasteries = SystemMasteries.All.Select(m => m.Name).Distinct().OrderBy(m => m).ToList();
|
|
ApplyFilters();
|
|
}
|
|
|
|
private void ApplyFilters()
|
|
{
|
|
_matchingSystemIds.Clear();
|
|
foreach (var system in _systems)
|
|
{
|
|
if (MatchesFilters(system))
|
|
{
|
|
_matchingSystemIds.Add(system.Id);
|
|
}
|
|
}
|
|
_matchingCount = _matchingSystemIds.Count;
|
|
}
|
|
|
|
private bool MatchesFilters(StarSystemData system)
|
|
{
|
|
if (_selectedCategory.HasValue && system.SystemCategory != _selectedCategory.Value)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (!string.IsNullOrEmpty(_selectedMastery))
|
|
{
|
|
if (_selectedMastery == "__ANY__")
|
|
{
|
|
if (string.IsNullOrWhiteSpace(system.SystemMastery)) return false;
|
|
}
|
|
else if (!string.Equals(system.SystemMastery, _selectedMastery, StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
if (!string.IsNullOrEmpty(_selectedYield))
|
|
{
|
|
if (_selectedYield == "ANY")
|
|
{
|
|
if (!system.HigherInfluenceYield && !system.HigherCreditYield && !system.HigherEnergyYield && !system.HigherChanceForItems)
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
else if (_selectedYield == "Influence" && !system.HigherInfluenceYield) return false;
|
|
else if (_selectedYield == "Credits" && !system.HigherCreditYield) return false;
|
|
else if (_selectedYield == "Energy" && !system.HigherEnergyYield) return false;
|
|
else if (_selectedYield == "Items" && !system.HigherChanceForItems) return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
private void ResetFilters()
|
|
{
|
|
_selectedCategory = null;
|
|
_selectedMastery = string.Empty;
|
|
_selectedYield = string.Empty;
|
|
ApplyFilters();
|
|
}
|
|
|
|
// ----- faction bonus drawer -----
|
|
|
|
public sealed record FactionBonus(string Label, bool IsPercent, double Total, int PlanetCount);
|
|
|
|
private void SelectFaction(string owner)
|
|
{
|
|
if (string.Equals(_selectedFaction, owner, StringComparison.Ordinal))
|
|
{
|
|
return;
|
|
}
|
|
|
|
_selectedFaction = owner;
|
|
}
|
|
|
|
private void ToggleDrawer() => _drawerCollapsed = !_drawerCollapsed;
|
|
|
|
private int FactionSystemCount(string owner) =>
|
|
_systems.Count(s => string.Equals(s.Owner, owner, StringComparison.OrdinalIgnoreCase));
|
|
|
|
private IReadOnlyList<StarSystemData> FactionSystems =>
|
|
_systems
|
|
.Where(s => string.Equals(s.Owner, _selectedFaction, StringComparison.OrdinalIgnoreCase))
|
|
.OrderBy(s => s.Name)
|
|
.ToList();
|
|
|
|
/// <summary>
|
|
/// Groups the selected faction's occupation buffs by bonus type and sums
|
|
/// the magnitudes, so e.g. two "+15% cooldown reduction" planets total +30%.
|
|
/// </summary>
|
|
private IReadOnlyList<FactionBonus> FactionBonuses
|
|
{
|
|
get
|
|
{
|
|
var groups = new Dictionary<(string Label, bool IsPercent), (double Total, int Count)>();
|
|
foreach (var system in _systems)
|
|
{
|
|
if (!string.Equals(system.Owner, _selectedFaction, StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (string.IsNullOrWhiteSpace(system.OccupationBuff))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var (amount, isPercent, label) = ParseOccupationBuff(system.OccupationBuff);
|
|
var key = (label, isPercent);
|
|
groups.TryGetValue(key, out var current);
|
|
groups[key] = (current.Total + amount, current.Count + 1);
|
|
}
|
|
|
|
return groups
|
|
.Select(g => new FactionBonus(g.Key.Label, g.Key.IsPercent, g.Value.Total, g.Value.Count))
|
|
.OrderByDescending(b => b.PlanetCount)
|
|
.ThenBy(b => b.Label)
|
|
.ToList();
|
|
}
|
|
}
|
|
|
|
/// <summary>Parses buffs shaped like "+50 Starting Hexite" or "+15% cooldown reduction".</summary>
|
|
private static (double Amount, bool IsPercent, string Label) ParseOccupationBuff(string buff)
|
|
{
|
|
var match = Regex.Match(buff.Trim(), @"^\+\s*([0-9]*\.?[0-9]+)\s*(%?)\s*(.+)$");
|
|
if (match.Success &&
|
|
double.TryParse(match.Groups[1].Value, NumberStyles.Float, CultureInfo.InvariantCulture, out var amount))
|
|
{
|
|
return (amount, match.Groups[2].Value == "%", match.Groups[3].Value.Trim());
|
|
}
|
|
|
|
return (0, false, buff.Trim());
|
|
}
|
|
|
|
private static string FormatBonusTotal(FactionBonus bonus) =>
|
|
$"+{bonus.Total.ToString("0.##", CultureInfo.InvariantCulture)}{(bonus.IsPercent ? "%" : string.Empty)}";
|
|
|
|
private static string ShortFactionName(string owner) => owner switch
|
|
{
|
|
StarSystemOwners.EdenAccord => "Eden Accord",
|
|
StarSystemOwners.KeeperCouncil => "Keeper Council",
|
|
StarSystemOwners.ObsidianOrder => "Obsidian Order",
|
|
_ => owner
|
|
};
|
|
|
|
private static string FactionDotClass(string owner) => owner switch
|
|
{
|
|
StarSystemOwners.EdenAccord => "dot-eden",
|
|
StarSystemOwners.KeeperCouncil => "dot-keeper",
|
|
StarSystemOwners.ObsidianOrder => "dot-obsidian",
|
|
_ => string.Empty
|
|
};
|
|
|
|
private static IReadOnlyList<ConnectionEdge> BuildUniqueConnections(IReadOnlyList<StarSystemData> allSystems)
|
|
{
|
|
var edges = new List<ConnectionEdge>();
|
|
var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
|
|
|
foreach (var system in allSystems)
|
|
{
|
|
foreach (var targetName in system.ConnectedSystems)
|
|
{
|
|
if (!StarSystems.ByName.TryGetValue(targetName, out var target))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var edgeKey = string.Compare(system.Id, target.Id, StringComparison.OrdinalIgnoreCase) < 0
|
|
? $"{system.Id}::{target.Id}"
|
|
: $"{target.Id}::{system.Id}";
|
|
|
|
if (seen.Add(edgeKey))
|
|
{
|
|
edges.Add(new ConnectionEdge(system, target));
|
|
}
|
|
}
|
|
}
|
|
|
|
return edges;
|
|
}
|
|
|
|
private void SetHoveredSystem(StarSystemData? system)
|
|
{
|
|
_hoveredSystem = system;
|
|
_hoveredConnectedIds.Clear();
|
|
if (system is not null)
|
|
{
|
|
foreach (var connName in system.ConnectedSystems)
|
|
{
|
|
if (StarSystems.ByName.TryGetValue(connName, out var target))
|
|
{
|
|
_hoveredConnectedIds.Add(target.Id);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private void OnNodeMouseEnter(StarSystemData system)
|
|
{
|
|
SetHoveredSystem(system);
|
|
}
|
|
|
|
private void OnNodeMouseLeave(StarSystemData system)
|
|
{
|
|
if (_hoveredSystem?.Id == system.Id)
|
|
{
|
|
SetHoveredSystem(null);
|
|
}
|
|
}
|
|
|
|
private void OnNodeFocus(StarSystemData system)
|
|
{
|
|
SetHoveredSystem(system);
|
|
}
|
|
|
|
private void OnNodeBlur()
|
|
{
|
|
SetHoveredSystem(null);
|
|
}
|
|
|
|
private void OnContainerMouseLeave()
|
|
{
|
|
SetHoveredSystem(null);
|
|
}
|
|
|
|
private void OnNodeKeyDown(KeyboardEventArgs e, string systemId)
|
|
{
|
|
if (e.Key is "Enter" or " ")
|
|
{
|
|
OnNodeClick(systemId);
|
|
}
|
|
}
|
|
|
|
private void OnNodeClick(string systemId)
|
|
{
|
|
Navigation.NavigateTo($"/StarSystem/{systemId}");
|
|
}
|
|
|
|
private static IReadOnlyList<string> GetHigherYields(StarSystemData system)
|
|
{
|
|
var yields = new List<string>(4);
|
|
if (system.HigherInfluenceYield) yields.Add("Influence");
|
|
if (system.HigherCreditYield) yields.Add("Credits");
|
|
if (system.HigherEnergyYield) yields.Add("Energy");
|
|
if (system.HigherChanceForItems) yields.Add("Items");
|
|
return yields;
|
|
}
|
|
|
|
private static string GetTooltipStyle(StarSystemData system)
|
|
{
|
|
var posX = system.X / 10.0;
|
|
var posY = system.Y / 10.0;
|
|
|
|
string transform;
|
|
|
|
if (posX > 65.0)
|
|
{
|
|
if (posY < 20.0)
|
|
{
|
|
transform = "translate(calc(-100% - 16px), 0%)";
|
|
}
|
|
else if (posY > 80.0)
|
|
{
|
|
transform = "translate(calc(-100% - 16px), -100%)";
|
|
}
|
|
else
|
|
{
|
|
transform = "translate(calc(-100% - 16px), -50%)";
|
|
}
|
|
}
|
|
else if (posX < 35.0)
|
|
{
|
|
if (posY < 20.0)
|
|
{
|
|
transform = "translate(16px, 0%)";
|
|
}
|
|
else if (posY > 80.0)
|
|
{
|
|
transform = "translate(16px, -100%)";
|
|
}
|
|
else
|
|
{
|
|
transform = "translate(16px, -50%)";
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (posY > 50.0)
|
|
{
|
|
transform = "translate(-50%, calc(-100% - 16px))";
|
|
}
|
|
else
|
|
{
|
|
transform = "translate(-50%, 16px)";
|
|
}
|
|
}
|
|
|
|
return $"left: {SvgNumber(posX)}%; top: {SvgNumber(posY)}%; transform: {transform};";
|
|
}
|
|
|
|
private static string GetOwnerCssClass(string owner) => owner switch
|
|
{
|
|
StarSystemOwners.EdenAccord => "owner-eden",
|
|
StarSystemOwners.KeeperCouncil => "owner-keeper",
|
|
StarSystemOwners.ObsidianOrder => "owner-obsidian",
|
|
_ => "owner-eden"
|
|
};
|
|
|
|
private static string CategoryBadgeClass(StarSystemCategory category) => category switch
|
|
{
|
|
StarSystemCategory.HomeworldSystem => "badge-homeworld",
|
|
StarSystemCategory.CoreSystem => "badge-core",
|
|
StarSystemCategory.SideSystem => "badge-side",
|
|
_ => "badge-side"
|
|
};
|
|
|
|
private static string OwnerBadgeClass(string owner) => owner switch
|
|
{
|
|
StarSystemOwners.EdenAccord => "badge-eden",
|
|
StarSystemOwners.KeeperCouncil => "badge-keeper",
|
|
StarSystemOwners.ObsidianOrder => "badge-obsidian",
|
|
_ => "badge-side"
|
|
};
|
|
|
|
private static double GetNodeRadius(StarSystemCategory category) => category switch
|
|
{
|
|
StarSystemCategory.HomeworldSystem => 7.0,
|
|
StarSystemCategory.CoreSystem => 6.0,
|
|
_ => 4.5
|
|
};
|
|
|
|
private static string SvgNumber(double value) => value.ToString(CultureInfo.InvariantCulture);
|
|
|
|
}
|