diff --git a/.junie/plans/galaxy-map-json-download.md b/.junie/plans/galaxy-map-json-download.md new file mode 100644 index 0000000..78456f9 --- /dev/null +++ b/.junie/plans/galaxy-map-json-download.md @@ -0,0 +1,123 @@ +--- +sessionId: session-260929-233614-1hed +--- + +# Requirements + +### Overview & Goals +Add a "Download JSON" button to the interactive Galaxy Map in `ZeroSpace.Web` that exports all data powering the galaxy map into a structured, downloadable JSON file (`galaxy-map-data.json`). The exported data will include all planetary systems with their full metadata and Markdown bodies, system masteries with their bonuses and notes, and planetary mastery items. + +### Scope +- **In Scope**: + - A client-side browser file download utility via Blazor `IJSRuntime`. + - Aggregation and serialization of `StarSystems.All` (including all markdown notes in `Body`), `SystemMasteries.All`, and `PlanetaryMasteryItems.All`. + - A "Download JSON" button styled with the project's Sci-Fi theme in `ZeroSpace.Web/Pages/Galaxy.razor`. +- **Out of Scope**: + - Backend API changes (the export is generated client-side from the static/in-memory data models already present in Blazor WebAssembly). + - Editing or mutating star system data. + +### User Stories +- **As a player / community developer**, I want to download all galaxy map data (including star systems, hyperlane connections, occupation buffs, masteries, and markdown notes) in JSON format from the Galaxy Map page so that I can analyze the data offline, create custom tooling, or reference game lore. + +### Functional Requirements +1. **Download Action**: Clicking the "Download JSON" button generates and initiates a browser download of `galaxy-map-data.json`. +2. **Star System Data & Markdown**: The JSON output must contain all star systems from `StarSystems.All` including ID, name, category, owner, yield flags, occupation buff, system mastery, connected systems, coordinates, and full markdown text (`Body`). +3. **Mastery Data**: The JSON output must contain all system masteries from `SystemMasteries.All` (levels, available systems, stats, and markdown body) and planetary mastery items from `PlanetaryMasteryItems.All`. +4. **JSON Formatting**: The output JSON must be well-formatted (indented, camelCase property naming, string representations for enums). +5. **UI Integration**: The button must be positioned intuitively in the Galaxy header / control bar matching the existing glassmorphic Sci-Fi styling. + +### Non-Functional Requirements +- **Performance**: Export generation and download initiation should take < 50ms without blocking UI interactions. +- **Offline / WebAssembly Compatibility**: The export must function entirely within the client-side Blazor WebAssembly runtime without requiring server endpoints. + +# Technical Design + +### Current Implementation +- `ZeroSpace.Web/Pages/Galaxy.razor`: The interactive galaxy map page displaying star systems, hyperlanes, and faction occupation details using `StarSystems.All` and `SystemMasteries.All`. +- `ZeroSpace.Model/StarSystemData.cs` & `StarSystems.g.cs`: Contains `StarSystemData` records where `Body` contains the planetary system Markdown notes. +- `ZeroSpace.Model/SystemMasteryData.cs` & `SystemMasteries.g.cs`: Contains `SystemMasteryData` records with mastery levels, system associations, stats, and markdown `Body`. +- `ZeroSpace.Model/PlanetaryMasteryItemData.cs` & `PlanetaryMasteryItems.g.cs`: Contains `PlanetaryMasteryItemData` records for mastery items. + +### Key Decisions +1. **Client-Side Export**: Use Blazor WebAssembly's `IJSRuntime` with a lightweight browser Blob URL helper to trigger file download instantly in memory without backend roundtrips. +2. **Unified Data Structure**: Group data under top-level properties (`systems`, `masteries`, `planetaryMasteryItems`, `exportedAt`) for easy consumption by external tools. +3. **Formatting**: Use `System.Text.Json` with `JsonSerializerOptions` configured for `WriteIndented = true`, `PropertyNamingPolicy = JsonNamingPolicy.CamelCase`, and `JsonStringEnumConverter`. + +### Proposed Changes +1. **File Download Helper**: + - Add `ZeroSpace.Web/wwwroot/js/fileDownload.js` containing `downloadFileFromText(fileName, contentType, textContent)`. + - Include script reference in `ZeroSpace.Web/wwwroot/index.html`. +2. **Export Model**: + - Define `GalaxyMapExportData` record containing `IReadOnlyList Systems`, `IReadOnlyList Masteries`, `IReadOnlyList PlanetaryMasteryItems`, and `DateTimeOffset ExportedAt`. +3. **Galaxy Page Update**: + - Add a "Download JSON" button to the header action bar in `Galaxy.razor`. + - Add `DownloadJsonAsync()` method in `@code` section that builds `GalaxyMapExportData`, serializes to JSON, and calls the JS helper. + +### Data Models / Contracts +```csharp +public sealed record GalaxyMapExportData( + DateTimeOffset ExportedAt, + int TotalSystems, + int TotalMasteries, + IReadOnlyList Systems, + IReadOnlyList Masteries, + IReadOnlyList PlanetaryMasteryItems); +``` + +### Architecture Diagram +```mermaid +graph LR + User[User clicks Download JSON] --> Razor[Galaxy.razor] + Razor --> Aggregator[Aggregate StarSystems, Masteries, Items] + Aggregator --> Serializer[System.Text.Json Serializer] + Serializer --> JSInterop[IJSRuntime downloadFileFromText] + JSInterop --> Browser[Browser saves galaxy-map-data.json] +``` + +### File Structure +- `ZeroSpace.Web/wwwroot/js/fileDownload.js` *(new)* +- `ZeroSpace.Web/wwwroot/index.html` *(modified to include script tag)* +- `ZeroSpace.Web/Models/GalaxyMapExportData.cs` *(new)* +- `ZeroSpace.Web/Pages/Galaxy.razor` *(modified to add download button and handler)* +- `ZeroSpace.Web/Pages/Galaxy.razor.css` *(optional style tweaks for button layout)* + +### Risks +- **Large Payload / Memory**: The total size of all systems and markdown is < 500 KB, which fits easily in browser memory for Blob generation. + +# Testing + +### Validation Approach +- Verify solution builds with 0 errors via `build_solution_start` and `build_solution_state`. +- Verify the serialized JSON structure against required fields (star system metadata, markdown `Body`, masteries, levels, stats). +- Verify the button UI placement, styling, and JSInterop invocation logic. + +### Key Scenarios +1. **Download Button Interaction**: User clicks the "Download JSON" button on `/galaxy`. + - Expected: `downloadFileFromText` JSInterop is called with `galaxy-map-data.json`, `application/json`, and the formatted JSON string. +2. **JSON Content Completeness**: + - Every star system from `StarSystems.All` is present in the `systems` array. + - The markdown `body` field of each star system is included. + - Every mastery from `SystemMasteries.All` is present in the `masteries` array with levels and stats. + - Planetary mastery items from `PlanetaryMasteryItems.All` are included. + +### Edge Cases +- Systems with empty vs populated markdown `Body` serialize properly as strings (`""` or markdown text). +- Enum values (`StarSystemCategory`) serialize cleanly as strings (e.g. `"homeworldSystem"`, `"coreSystem"`, `"sideSystem"` or PascalCase). + +# Delivery Steps + +### ✓ Step 1: Implement file download JSInterop helper and export serialization model +Create the file download JSInterop utility and the galaxy map data export serialization contract. + +- Add a JavaScript file download helper (`ZeroSpace.Web/wwwroot/js/fileDownload.js` or equivalent) that accepts filename, content type, and content/bytes, creating an object URL blob and triggering a download anchor click. +- Reference the script in `ZeroSpace.Web/wwwroot/index.html`. +- Define an export container record (e.g., `GalaxyMapExportData`) in `ZeroSpace.Web` / `ZeroSpace.Model` that aggregates star systems (with full metadata and markdown `Body`), system masteries (with levels, stats, available systems, and `Body`), and planetary mastery items. +- Implement a JSON serialization helper/service configured with `JsonSerializerOptions` (`WriteIndented = true`, camelCase naming, `JsonStringEnumConverter`) for clean formatting. + +### ✓ Step 2: Integrate Download JSON button and handler into the Galaxy Map page +Add the Download JSON button to the Galaxy Map page and wire up the export trigger. + +- Add the "Download JSON" button in the top action bar of `ZeroSpace.Web/Pages/Galaxy.razor` next to the map legend/stats controls. +- Apply consistent Sci-Fi design system styling (`btn-scifi-outline` with appropriate icon/label) in `Galaxy.razor` and `Galaxy.razor.css`. +- Inject `IJSRuntime` in `Galaxy.razor` and implement the `DownloadGalaxyJsonAsync` event handler to generate the export payload and trigger the client-side download named `galaxy-map-data.json`. +- Build the solution and verify clean compilation without errors or warnings. \ No newline at end of file diff --git a/ZeroSpace/ZeroSpace.Model/GalaxyMapExportData.cs b/ZeroSpace/ZeroSpace.Model/GalaxyMapExportData.cs new file mode 100644 index 0000000..52d4646 --- /dev/null +++ b/ZeroSpace/ZeroSpace.Model/GalaxyMapExportData.cs @@ -0,0 +1,19 @@ +namespace ZeroSpace.Model; + +public sealed record GalaxyMapExportData( + DateTimeOffset ExportedAt, + int TotalSystems, + int TotalMasteries, + IReadOnlyList Systems, + IReadOnlyList Masteries, + IReadOnlyList PlanetaryMasteryItems) +{ + public static GalaxyMapExportData CreateDefault() => + new( + DateTimeOffset.UtcNow, + StarSystems.All.Count, + SystemMasteries.All.Count, + StarSystems.All, + SystemMasteries.All, + ZeroSpace.Model.PlanetaryMasteryItems.All); +} \ No newline at end of file diff --git a/ZeroSpace/ZeroSpace.Web/Pages/Galaxy.razor b/ZeroSpace/ZeroSpace.Web/Pages/Galaxy.razor index cf9dcea..2f775e2 100644 --- a/ZeroSpace/ZeroSpace.Web/Pages/Galaxy.razor +++ b/ZeroSpace/ZeroSpace.Web/Pages/Galaxy.razor @@ -2,6 +2,8 @@ @using System.Globalization @using System.Text.RegularExpressions @inject NavigationManager Navigation +@inject IJSRuntime Js +@inject IGalaxyMapExportSerializer ExportSerializer Galaxy Map @@ -9,7 +11,8 @@

Galaxy Map

-

Interactive tactical star chart displaying all planetary systems and hyperlane routes.

+

Interactive tactical star chart displaying all planetary systems and + hyperlane routes.

@@ -34,6 +37,19 @@ • @_connections.Count Hyperlanes
+
@@ -41,7 +57,8 @@
- @@ -69,7 +86,7 @@ - +
@@ -83,263 +100,272 @@
- - - - - - + + + + + + - - - - - - + + + + + + - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + - - - - + + + + - - - @foreach (var edge in _connections) - { - var isFromMatch = _matchingSystemIds.Contains(edge.From.Id); - var isToMatch = _matchingSystemIds.Contains(edge.To.Id); - var isFilterDimmed = HasActiveFilters && (!isFromMatch || !isToMatch); + + + @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"; - - } - + var lineClass = isHoverHighlighted ? "hyperlane-line hyperlane-highlighted" : + (isHoverDimmed || isFilterDimmed) ? "hyperlane-line hyperlane-dimmed" : "hyperlane-line"; + + } + - - - @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" : ""); + + + @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" : ""); - + - - + + - - + + - - @if (system.SystemCategory != StarSystemCategory.SideSystem) - { - - } + + @if (system.SystemCategory != StarSystemCategory.SideSystem) + { + + } - - @system.Name - - } - - + + @system.Name + + } + + - - @if (_hoveredSystem is not null) - { - var ownerClass = GetOwnerCssClass(_hoveredSystem.Owner); - var higherYields = GetHigherYields(_hoveredSystem); -