Io/zerospace converter #1

Merged
JonathanMcCaffrey merged 137 commits from io/zerospace-converter into master 2026-09-29 18:21:24 +00:00
Showing only changes of commit beb22d097f - Show all commits
+174 -21
View File
@@ -1,5 +1,6 @@
@page "/galaxy"
@using System.Globalization
@using System.Text.RegularExpressions
@inject NavigationManager Navigation
<PageTitle>Galaxy Map</PageTitle>
@@ -72,17 +73,6 @@
</select>
</div>
<div class="d-flex align-items-center gap-2 filter-item">
<label class="filter-label text-dim small fw-semibold text-uppercase">Faction:</label>
<select class="form-select form-select-sm scifi-select" @bind="_selectedOwner" @bind:after="ApplyFilters">
<option value="">All Factions</option>
@foreach (var owner in StarSystemOwners.All)
{
<option value="@owner">@owner</option>
}
</select>
</div>
@if (HasActiveFilters)
{
<button class="btn btn-sm btn-scifi-outline py-1 px-3 ms-auto" @onclick="ResetFilters">
@@ -91,7 +81,8 @@
}
</div>
<div class="galaxy-map-container" @onmouseleave="OnContainerMouseLeave">
<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>
@@ -276,6 +267,80 @@
</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 {
@@ -292,14 +357,16 @@
private StarSystemCategory? _selectedCategory;
private string _selectedMastery = string.Empty;
private string _selectedYield = string.Empty;
private string _selectedOwner = 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) ||
!string.IsNullOrEmpty(_selectedOwner);
!string.IsNullOrEmpty(_selectedYield);
protected override void OnInitialized()
{
@@ -356,11 +423,6 @@
else if (_selectedYield == "Items" && !system.HigherChanceForItems) return false;
}
if (!string.IsNullOrEmpty(_selectedOwner) && !string.Equals(system.Owner, _selectedOwner, StringComparison.OrdinalIgnoreCase))
{
return false;
}
return true;
}
@@ -369,10 +431,101 @@
_selectedCategory = null;
_selectedMastery = string.Empty;
_selectedYield = string.Empty;
_selectedOwner = 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>();