Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0a72031265 | ||
|
|
c78eeca0f6 | ||
|
|
2dbef9d079 | ||
|
|
715b897894 | ||
|
|
a19af31db5 | ||
|
|
18f2389e06 | ||
|
|
ac77708da2 | ||
|
|
eb9e2a7035 | ||
|
|
45c1c59a8d | ||
|
|
fc58e62d77 | ||
|
|
c55d021c43 | ||
|
|
33ac8b0d41 | ||
|
|
ccc55a5a32 | ||
|
|
d62f2f810a | ||
|
|
3484317567 | ||
|
|
375b07284b | ||
|
|
bb9413aee7 | ||
|
|
d6341b0143 | ||
|
|
51f3e0c380 | ||
|
|
697b714de8 | ||
|
|
e243c855c1 | ||
|
|
a60c82fc5f | ||
|
|
c2a7eed511 | ||
|
|
22b25976a0 | ||
|
|
63d07bb8bd | ||
|
|
c9fde9de15 | ||
|
|
0632d70631 | ||
|
|
4414256e53 | ||
|
|
010c2468f4 | ||
|
|
d4b8a579f9 | ||
|
|
e8c7341e3e | ||
|
|
caca793d30 | ||
|
|
77f2564b55 | ||
|
|
e54759c1dd | ||
|
|
77274cbb35 | ||
|
|
72808e8430 | ||
|
|
9f6cf22023 | ||
|
|
8b0764c85a | ||
|
|
f02a921a89 | ||
|
|
f6e1308950 | ||
|
|
9aee7af7c7 | ||
|
|
6410b2ff29 | ||
|
|
432dec4ada | ||
|
|
373b685797 | ||
|
|
1ed83a8b54 | ||
|
|
d29b82f14f | ||
|
|
d5bd5fc75f | ||
|
|
2d0095bcbe | ||
|
|
70605fa855 | ||
|
|
5204025394 | ||
|
|
80a44e776c | ||
|
|
fe8709085b | ||
|
|
119111051c | ||
|
|
0bbab8bd02 | ||
|
|
212ad4f332 | ||
|
|
439d6708b4 | ||
|
|
03f2cca35e | ||
|
|
ba3a70ef7a | ||
|
|
ae7f217b5d | ||
|
|
6991b49c5d | ||
|
|
97f7259a66 | ||
|
|
4da5e996fe | ||
|
|
7603da1032 | ||
|
|
741fa08d05 | ||
|
|
3a10b8d043 | ||
|
|
aacb40a492 | ||
|
|
c6dcc61032 | ||
|
|
4d8f19f1e1 | ||
|
|
1044050540 | ||
|
|
b1bd90b248 | ||
|
|
e8db7f7894 | ||
|
|
fa8832f266 | ||
|
|
678a45e432 | ||
|
|
f88e3a6224 | ||
|
|
31d521b52b | ||
|
|
75393943dd | ||
|
|
8a9f135118 | ||
|
|
65dc2edcb0 | ||
|
|
6f25d92b1e | ||
|
|
f7803c5935 | ||
|
|
d6a96e8708 | ||
|
|
01aeb3eda0 | ||
|
|
8dee3cbe05 | ||
|
|
ee77d44659 | ||
|
|
aa819f4581 | ||
|
|
761b8f3d05 | ||
|
|
a6db0075da | ||
|
|
b4209f5a88 | ||
|
|
7417c08a6a | ||
|
|
42b8a74c86 | ||
|
|
ccc17b6751 | ||
|
|
0758bed6ae | ||
|
|
14ce76212a | ||
|
|
0a07592333 | ||
|
|
03aa85b6d6 | ||
|
|
534b98832d | ||
|
|
ef45da37ac | ||
|
|
f5a489a495 | ||
|
|
8fb65abfb7 | ||
|
|
21feb463cf | ||
|
|
1f2d4d6689 | ||
|
|
24fde52b9a | ||
|
|
4e0a9c13bf | ||
|
|
66f6494f5d | ||
|
|
57c41a50cc | ||
|
|
18d228c03c | ||
|
|
130b646215 | ||
|
|
fdec2fcd9a | ||
|
|
33012bfe2b | ||
|
|
f8783659a1 | ||
|
|
fb2222c7a9 | ||
|
|
eebfd5728f | ||
|
|
572747df6a | ||
|
|
c5a9c3d029 | ||
|
|
5f98b6c568 | ||
|
|
55b9350d1b | ||
|
|
e65039d279 | ||
|
|
3b78411153 | ||
|
|
5d11ce5fa1 | ||
|
|
1c55d563f5 | ||
|
|
4d12f134c7 | ||
|
|
f1f35030e9 | ||
|
|
44fa91b524 | ||
|
|
425bdcc170 | ||
|
|
011981488c | ||
|
|
a93570e847 | ||
|
|
c6b8015caa | ||
|
|
caeefc9220 | ||
|
|
6d0c4e0e0c | ||
|
|
7dc49b3818 | ||
|
|
dd4876a4bd | ||
|
|
1758a7c5ba | ||
|
|
d00e41571f | ||
|
|
a0fa28f753 | ||
|
|
dde00753bf | ||
|
|
28f47879f2 | ||
|
|
6169dc2fff | ||
|
|
e9a8302a01 | ||
|
|
6f13fd351c | ||
|
|
ec5d464bdf | ||
|
|
1b65434faa | ||
|
|
8d39b3fc54 | ||
|
|
d4db96a389 | ||
|
|
9de5b38845 | ||
|
|
7386327acb | ||
|
|
b52dd82913 | ||
|
|
2a630577fe | ||
|
|
5b5ec830e4 | ||
|
|
9f8c149604 | ||
|
|
c84b034604 | ||
|
|
0914d763d0 | ||
|
|
25506b4b87 | ||
|
|
3d5d1d1acd | ||
|
|
5348842d0c | ||
|
|
a50f45a06a | ||
|
|
278aa03c1c | ||
|
|
4329299d36 | ||
|
|
0a6e7ddd9b | ||
|
|
d0cec56be5 | ||
|
|
a42788c382 | ||
|
|
10c2dcb938 | ||
|
|
3de2537252 | ||
|
|
067a164747 | ||
|
|
7fd003d707 | ||
|
|
8f6e47f0d3 | ||
|
|
d0fe909ae0 | ||
|
|
7212861fd5 | ||
|
|
85fa0fe29e | ||
|
|
c0b01b4a49 | ||
|
|
3b1cd8f849 | ||
|
|
1d75a744ba | ||
|
|
3cad155cc8 | ||
|
|
207322620b | ||
|
|
baf21465aa | ||
|
|
fed6e8627c | ||
|
|
d318a8c3fe | ||
|
|
71fc420321 | ||
|
|
2cb6221e10 | ||
|
|
14b39c15ec | ||
|
|
ab7a0e435a | ||
|
|
eb7110d3e0 | ||
|
|
c425752c8d | ||
|
|
fd023ba3bf | ||
|
|
9f457005d8 | ||
|
|
4306bea6f8 | ||
|
|
8751c8ac08 | ||
|
|
1c3671ad13 | ||
|
|
0349ebf2bc | ||
|
|
e0e147e2c4 | ||
|
|
dde2bac522 | ||
|
|
b43dd4ad26 | ||
|
|
9f8bd6ab15 | ||
|
|
bdfeaca09b | ||
|
|
ea771d8c57 | ||
|
|
4f92ea7ae8 | ||
|
|
f2e3667f1c | ||
|
|
7e61868750 | ||
|
|
2f38b4b781 | ||
|
|
fbc6c48ce9 | ||
|
|
14ddeab1b7 | ||
|
|
30b56060dd |
@@ -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<StarSystemData> Systems`, `IReadOnlyList<SystemMasteryData> Masteries`, `IReadOnlyList<PlanetaryMasteryItemData> 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<StarSystemData> Systems,
|
||||||
|
IReadOnlyList<SystemMasteryData> Masteries,
|
||||||
|
IReadOnlyList<PlanetaryMasteryItemData> 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.
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"route": "/index.html",
|
||||||
|
"headers": {
|
||||||
|
"Cache-Control": "no-cache"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"route": "/_framework/*",
|
||||||
|
"headers": {
|
||||||
|
"Cache-Control": "public, max-age=31536000, immutable"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"navigationFallback": {
|
||||||
|
"rewrite": "/index.html"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -44,7 +44,7 @@ The goal is to modernize the ZeroSpace application by introducing **.NET Aspire*
|
|||||||
# Technical Design
|
# Technical Design
|
||||||
|
|
||||||
### Current Implementation
|
### Current Implementation
|
||||||
- `ZeroSpace.Web` is a standalone Blazor WebAssembly application (`Microsoft.NET.Sdk.BlazorWebAssembly`) targeting .NET 10 with Telerik UI for Blazor.
|
- `ZeroSpace.Web` is a standalone Blazor WebAssembly application (`Microsoft.NET.Sdk.BlazorWebAssembly`) targeting .NET 10, using native Blazor UI components only.
|
||||||
- `ZeroSpace.Model` contains records like `UnitData.cs`, `UnitEfficiency.cs`, and generated static unit arrays in `Units.g.cs`.
|
- `ZeroSpace.Model` contains records like `UnitData.cs`, `UnitEfficiency.cs`, and generated static unit arrays in `Units.g.cs`.
|
||||||
- `ZeroSpace.Deploy` publishes static files to Azure Static Web Apps.
|
- `ZeroSpace.Deploy` publishes static files to Azure Static Web Apps.
|
||||||
- `ZeroSpace.AppHost`, `ZeroSpace.ServiceDefaults`, and `ZeroSpace.ApiService` are integrated into the solution.
|
- `ZeroSpace.AppHost`, `ZeroSpace.ServiceDefaults`, and `ZeroSpace.ApiService` are integrated into the solution.
|
||||||
|
|||||||
@@ -0,0 +1,176 @@
|
|||||||
|
---
|
||||||
|
sessionId: session-260930-201705-1ilk
|
||||||
|
---
|
||||||
|
|
||||||
|
# Requirements
|
||||||
|
|
||||||
|
### Overview & Goals
|
||||||
|
The objective is to ensure that deep link URLs (such as `https://zs.gameslopreference.ca/galaxy`, `/planetary-mastery-items`, `/StarSystem/...`, and `/SystemMastery/...`) load properly when requested directly or refreshed in production browsers, rather than resulting in 404 Not Found errors.
|
||||||
|
|
||||||
|
Providing robust URL resolution maximizes utility for end users by enabling direct bookmarking, shareable links, and reliable browser refreshes across deployment targets including Azure Static Web Apps and static hosting environments.
|
||||||
|
|
||||||
|
### Scope
|
||||||
|
- **In Scope:**
|
||||||
|
- Configuring Azure Static Web Apps navigation fallback rules (`staticwebapp.config.json`) in `ZeroSpace.Web/wwwroot`.
|
||||||
|
- Adding universal static host SPA 404 redirect fallback (`404.html`) and client-side URL restoration in `ZeroSpace.Web/wwwroot/index.html`.
|
||||||
|
- Ensuring static assets (images, CSS, JS, WASM binaries) remain directly accessible and properly cached without being captured by fallback rewrites.
|
||||||
|
- Verifying publish and deployment compatibility with `ZeroSpace.Deploy`.
|
||||||
|
|
||||||
|
- **Out of Scope:**
|
||||||
|
- Modifying backend APIs or database schemas in `ZeroSpace.ApiService`.
|
||||||
|
- Altering Blazor internal routing logic in `App.razor` or `ReleaseGuard.razor`.
|
||||||
|
|
||||||
|
### User Stories
|
||||||
|
- As a user navigating directly to `https://zs.gameslopreference.ca/galaxy`, I want the galaxy map page to render directly without encountering a host 404 error.
|
||||||
|
- As a user sharing or bookmarking a deep link to a star system or planetary mastery item, I want the recipient to immediately load the exact target route.
|
||||||
|
- As an administrator refreshing a deep page URL, I want the session and route to remain intact with zero broken navigation flows.
|
||||||
|
|
||||||
|
### Functional Requirements
|
||||||
|
- Direct GET requests to client-side routes (e.g., `/galaxy`, `/planetary-mastery-items`) must return the main application shell (`index.html`) with HTTP 200/redirect so the Blazor WebAssembly client router can take over.
|
||||||
|
- Requests for physical static files under `/images/`, `/css/`, `/js/`, `/lib/`, and `/_framework/` must continue to be served directly as static files or return standard 404s if missing, avoiding infinite loop rewrites.
|
||||||
|
- URL query parameters and hash fragments must be preserved when client-side redirects occur.
|
||||||
|
|
||||||
|
### Non-Functional Requirements
|
||||||
|
- **Performance & Reliability:** The redirect mechanism must execute with negligible overhead (<5ms execution time) before WebAssembly startup.
|
||||||
|
- **Portability:** The solution must function seamlessly whether deployed via Azure Static Web Apps (`swa deploy`), containerized Nginx (`ZeroSpace.Web/nginx.conf`), or static CDN hosts.
|
||||||
|
|
||||||
|
# Technical Design
|
||||||
|
|
||||||
|
### Current Implementation
|
||||||
|
- `ZeroSpace.Web` is a client-side Blazor WebAssembly application (`Microsoft.NET.Sdk.BlazorWebAssembly`) targeting .NET 10.
|
||||||
|
- `ZeroSpace.Deploy` publishes the Web project to a temporary directory and deploys `wwwroot` to Azure Static Web Apps using `swa deploy`.
|
||||||
|
- While `ZeroSpace.Web/nginx.conf` contains `try_files $uri $uri/ /index.html;` for Docker deployments, Azure Static Web Apps lacks a `staticwebapp.config.json` configuration, causing direct HTTP requests to deep paths like `/galaxy` to return 404 responses from Azure's edge servers.
|
||||||
|
- `ZeroSpace.Web/wwwroot/index.html` currently lacks client-side deep URL reconstruction script handlers for static host fallbacks.
|
||||||
|
|
||||||
|
### Key Decisions
|
||||||
|
- **Dual-Layer Fallback Strategy (Azure SWA + Universal SPA Redirect):**
|
||||||
|
- *Decision:* Provide both `staticwebapp.config.json` for native Azure Static Web Apps server-side rewrites and `404.html` + `index.html` URL restoration script for generic static hosting.
|
||||||
|
- *Rationale:* Maximizes availability and utility across all deployment topologies while delivering instant, clean URL serving on Azure SWA without redirect hops.
|
||||||
|
|
||||||
|
- **Explicit Asset Exclusion in Navigation Fallback:**
|
||||||
|
- *Decision:* Explicitly exclude `_framework`, `images`, `css`, `js`, and `lib` directories from navigation fallback rewriting.
|
||||||
|
- *Rationale:* Prevents corrupted binary downloads (such as `.wasm` or missing images) from accidentally serving `index.html` content with invalid MIME types.
|
||||||
|
|
||||||
|
### Proposed Changes
|
||||||
|
|
||||||
|
#### 1. Add `ZeroSpace.Web/wwwroot/staticwebapp.config.json`
|
||||||
|
Configure the Azure Static Web Apps routing engine:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"navigationFallback": {
|
||||||
|
"rewrite": "/index.html",
|
||||||
|
"exclude": [
|
||||||
|
"/images/*",
|
||||||
|
"/css/*",
|
||||||
|
"/js/*",
|
||||||
|
"/lib/*",
|
||||||
|
"/_framework/*",
|
||||||
|
"/*.ico",
|
||||||
|
"/*.png",
|
||||||
|
"/*.json"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"responseOverrides": {
|
||||||
|
"404": {
|
||||||
|
"rewrite": "/index.html"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"mimeTypes": {
|
||||||
|
".wasm": "application/wasm",
|
||||||
|
".json": "application/json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2. Add `ZeroSpace.Web/wwwroot/404.html`
|
||||||
|
Add lightweight SPA 404 redirect handler for static hosts:
|
||||||
|
```html
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<title>ZeroSpace</title>
|
||||||
|
<script type="text/javascript">
|
||||||
|
var pathSegmentsToKeep = 0;
|
||||||
|
var l = window.location;
|
||||||
|
l.replace(
|
||||||
|
l.protocol + '//' + l.hostname + (l.port ? ':' + l.port : '') +
|
||||||
|
l.pathname.split('/').slice(0, 1 + pathSegmentsToKeep).join('/') + '/?/' +
|
||||||
|
l.pathname.slice(1).split('/').slice(pathSegmentsToKeep).join('/').replace(/&/g, '~and~') +
|
||||||
|
(l.search ? '&' + l.search.slice(1).replace(/&/g, '~and~') : '') +
|
||||||
|
l.hash
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 3. Update `ZeroSpace.Web/wwwroot/index.html`
|
||||||
|
Add the URL reconstruction handler before Blazor script startup in `<head>`:
|
||||||
|
```html
|
||||||
|
<script type="text/javascript">
|
||||||
|
(function(l) {
|
||||||
|
if (l.search[1] === '/' ) {
|
||||||
|
var decoded = l.search.slice(1).split('&').map(function(s) {
|
||||||
|
return s.replace(/~and~/g, '&')
|
||||||
|
}).join('?');
|
||||||
|
window.history.replaceState(null, null,
|
||||||
|
l.pathname.slice(0, -1) + decoded + l.hash
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}(window.location))
|
||||||
|
</script>
|
||||||
|
```
|
||||||
|
|
||||||
|
### File Structure
|
||||||
|
- `ZeroSpace.Web/`
|
||||||
|
- `wwwroot/`
|
||||||
|
- `staticwebapp.config.json` *(new)*
|
||||||
|
- `404.html` *(new)*
|
||||||
|
- `index.html` *(modified)*
|
||||||
|
|
||||||
|
### Risks & Mitigations
|
||||||
|
- **Static Asset Masking:** If asset routes are mistakenly rewritten to `index.html`, WASM or image loads fail silently.
|
||||||
|
- *Mitigation:* Explicit glob exclusions in `staticwebapp.config.json` for `/_framework/*`, `/images/*`, `/css/*`, and asset extensions.
|
||||||
|
- **Route Guard Interaction:** Unreleased pages should still respect release restrictions.
|
||||||
|
- *Mitigation:* `ReleaseGuard.razor` continues to evaluate `SiteMap.IsReleased(pageType)` after router loads, gracefully redirecting unreleased routes to `/galaxy`.
|
||||||
|
|
||||||
|
# Testing
|
||||||
|
|
||||||
|
### Validation Approach
|
||||||
|
Verification focuses on confirming that published artifacts contain all necessary configuration files, that the build succeeds without error, and that URL restoration logic accurately handles routes, query strings, and asset exclusions.
|
||||||
|
|
||||||
|
### Key Scenarios
|
||||||
|
- **Direct Route Navigation:** Direct GET to `/galaxy` serves the application shell and initializes Blazor at `/galaxy`.
|
||||||
|
- **Deep Item Route Navigation:** Direct GET to `/planetary-mastery-items` or `/StarSystem` routes directly to the requested component.
|
||||||
|
- **Static Asset Serving:** Requests to `/_framework/blazor.webassembly.js`, `/images/planetary-mastery-items/Battery.png`, and `/css/app.css` return the physical static files directly with correct MIME types.
|
||||||
|
- **Query Parameter Preservation:** Direct URL with queries (e.g. `/galaxy?filter=core`) retains the query parameters after URL reconstruction.
|
||||||
|
|
||||||
|
### Test Changes
|
||||||
|
- Execute `dotnet build ZeroSpace.sln` to ensure all projects build cleanly.
|
||||||
|
- Execute `dotnet publish ZeroSpace.Web/ZeroSpace.Web.csproj -c Release` and inspect the output directory to confirm `staticwebapp.config.json`, `404.html`, and `index.html` are copied to `publish/wwwroot`.
|
||||||
|
|
||||||
|
# Delivery Steps
|
||||||
|
|
||||||
|
### Step 1: Configure Azure Static Web Apps navigation fallback
|
||||||
|
Azure Static Web Apps routes direct SPA page requests to `/index.html` while preserving direct access to static assets.
|
||||||
|
|
||||||
|
- Create `ZeroSpace.Web/wwwroot/staticwebapp.config.json` with `navigationFallback.rewrite` set to `/index.html`.
|
||||||
|
- Add route exclusions for static assets (`/images/*`, `/css/*`, `/js/*`, `/lib/*`, `/_framework/*`, and common file extensions).
|
||||||
|
- Define MIME type overrides for `.wasm` and `.json` to ensure optimal loading performance.
|
||||||
|
|
||||||
|
### Step 2: Implement universal 404 SPA redirect and URL reconstruction
|
||||||
|
Static hosts without native rewrite capabilities redirect deep link 404s back to `index.html` where the original URL path is reconstructed seamlessly.
|
||||||
|
|
||||||
|
- Add `ZeroSpace.Web/wwwroot/404.html` containing the lightweight SPA redirect script that converts deep URL paths into query parameters.
|
||||||
|
- Update `ZeroSpace.Web/wwwroot/index.html` `<head>` section with the matching URL restoration script using `history.replaceState` before the Blazor WebAssembly runtime initializes.
|
||||||
|
- Ensure query strings and route hash fragments are preserved intact during URL reconstruction.
|
||||||
|
|
||||||
|
### Step 3: Validate publish output and build verification
|
||||||
|
The project builds and publishes cleanly with all fallback and redirect assets bundled in the output `wwwroot`.
|
||||||
|
|
||||||
|
- Run `dotnet build ZeroSpace.sln` to confirm compilation integrity.
|
||||||
|
- Run `dotnet publish ZeroSpace.Web/ZeroSpace.Web.csproj -c Release` to verify that `staticwebapp.config.json`, `404.html`, and `index.html` are correctly packaged into the published output directory.
|
||||||
|
- Verify that `ZeroSpace.Deploy/Program.cs` can deploy the published `wwwroot` artifact containing the fallback configurations.
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<configuration>
|
|
||||||
<packageSources>
|
|
||||||
<add key="Telerik Blazor 13.0.0" value="C:\Program Files (x86)\Progress\Telerik UI for Blazor 13.0.0" />
|
|
||||||
</packageSources>
|
|
||||||
</configuration>
|
|
||||||
@@ -11,8 +11,8 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Aspire.Hosting.AppHost" Version="13.5.4" />
|
<PackageReference Include="Aspire.Hosting.AppHost" Version="13.6.0" />
|
||||||
<PackageReference Include="Aspire.Hosting.PostgreSQL" Version="13.5.4" />
|
<PackageReference Include="Aspire.Hosting.PostgreSQL" Version="13.6.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@@ -71,6 +71,9 @@ if (skipped.Count > 0)
|
|||||||
|
|
||||||
var starSystems = new List<StarSystemSource>();
|
var starSystems = new List<StarSystemSource>();
|
||||||
var systemMasteries = new List<SystemMasterySource>();
|
var systemMasteries = new List<SystemMasterySource>();
|
||||||
|
var planetaryMasteryItems = new List<PlanetaryMasteryItemSource>();
|
||||||
|
var generalItems = new List<GeneralItemSource>();
|
||||||
|
var factionPowerItems = new List<FactionPowerItemSource>();
|
||||||
var skippedDocs = new List<string>();
|
var skippedDocs = new List<string>();
|
||||||
var usedIds = new HashSet<string>(StringComparer.Ordinal);
|
var usedIds = new HashSet<string>(StringComparer.Ordinal);
|
||||||
|
|
||||||
@@ -104,19 +107,85 @@ foreach (var path in Directory.EnumerateFiles(docsRoot, "*.md", SearchOption.All
|
|||||||
skippedDocs.Add($"{Path.GetFileName(path)} ({reason})");
|
skippedDocs.Add($"{Path.GetFileName(path)} ({reason})");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
else if (string.Equals(categoryValue.Scalar, "PlanetaryMasteryItem", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
if (TryReadPlanetaryMasteryItem(path, frontMatter, body, usedIds, out var item, out var reason))
|
||||||
|
{
|
||||||
|
planetaryMasteryItems.Add(item);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
skippedDocs.Add($"{Path.GetFileName(path)} ({reason})");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (string.Equals(categoryValue.Scalar, "GeneralItem", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
if (TryReadGeneralItem(path, frontMatter, body, usedIds, out var generalItem, out var reason))
|
||||||
|
{
|
||||||
|
generalItems.Add(generalItem);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
skippedDocs.Add($"{Path.GetFileName(path)} ({reason})");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (string.Equals(categoryValue.Scalar, "FactionPowerItem", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
if (TryReadFactionPowerItem(path, frontMatter, body, usedIds, out var factionPowerItem, out var reason))
|
||||||
|
{
|
||||||
|
factionPowerItems.Add(factionPowerItem);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
skippedDocs.Add($"{Path.GetFileName(path)} ({reason})");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
starSystems.Sort((left, right) => string.Compare(left.Name, right.Name, StringComparison.OrdinalIgnoreCase));
|
starSystems.Sort((left, right) => string.Compare(left.Name, right.Name, StringComparison.OrdinalIgnoreCase));
|
||||||
systemMasteries.Sort((left, right) => string.Compare(left.Name, right.Name, StringComparison.OrdinalIgnoreCase));
|
systemMasteries.Sort((left, right) => string.Compare(left.Name, right.Name, StringComparison.OrdinalIgnoreCase));
|
||||||
|
planetaryMasteryItems.Sort((left, right) => string.Compare(left.Name, right.Name, StringComparison.OrdinalIgnoreCase));
|
||||||
|
generalItems.Sort((left, right) => string.Compare(left.Name, right.Name, StringComparison.OrdinalIgnoreCase));
|
||||||
|
factionPowerItems.Sort((left, right) => string.Compare(left.Name, right.Name, StringComparison.OrdinalIgnoreCase));
|
||||||
|
|
||||||
var starSystemsPath = Path.Combine(modelRoot, "StarSystems.g.cs");
|
var starSystemsPath = Path.Combine(modelRoot, "StarSystems.g.cs");
|
||||||
var systemMasteriesPath = Path.Combine(modelRoot, "SystemMasteries.g.cs");
|
var systemMasteriesPath = Path.Combine(modelRoot, "SystemMasteries.g.cs");
|
||||||
|
var planetaryMasteryItemsPath = Path.Combine(modelRoot, "PlanetaryMasteryItems.g.cs");
|
||||||
|
var generalItemsPath = Path.Combine(modelRoot, "GeneralItems.g.cs");
|
||||||
|
var factionPowerItemsPath = Path.Combine(modelRoot, "FactionPowerItems.g.cs");
|
||||||
|
|
||||||
File.WriteAllText(starSystemsPath, GenerateStarSystems(starSystems), Encoding.UTF8);
|
File.WriteAllText(starSystemsPath, GenerateStarSystems(starSystems), Encoding.UTF8);
|
||||||
File.WriteAllText(systemMasteriesPath, GenerateSystemMasteries(systemMasteries), Encoding.UTF8);
|
File.WriteAllText(systemMasteriesPath, GenerateSystemMasteries(systemMasteries), Encoding.UTF8);
|
||||||
|
File.WriteAllText(planetaryMasteryItemsPath, GeneratePlanetaryMasteryItems(planetaryMasteryItems), Encoding.UTF8);
|
||||||
|
File.WriteAllText(generalItemsPath, GenerateGeneralItems(generalItems), Encoding.UTF8);
|
||||||
|
File.WriteAllText(factionPowerItemsPath, GenerateFactionPowerItems(factionPowerItems), Encoding.UTF8);
|
||||||
|
|
||||||
Console.WriteLine($"Generated {starSystems.Count} star systems: {Path.GetRelativePath(solutionRoot, starSystemsPath)}");
|
Console.WriteLine($"Generated {starSystems.Count} star systems: {Path.GetRelativePath(solutionRoot, starSystemsPath)}");
|
||||||
Console.WriteLine($"Generated {systemMasteries.Count} system masteries: {Path.GetRelativePath(solutionRoot, systemMasteriesPath)}");
|
Console.WriteLine($"Generated {systemMasteries.Count} system masteries: {Path.GetRelativePath(solutionRoot, systemMasteriesPath)}");
|
||||||
|
Console.WriteLine($"Generated {planetaryMasteryItems.Count} planetary mastery items: {Path.GetRelativePath(solutionRoot, planetaryMasteryItemsPath)}");
|
||||||
|
Console.WriteLine($"Generated {generalItems.Count} general items: {Path.GetRelativePath(solutionRoot, generalItemsPath)}");
|
||||||
|
Console.WriteLine($"Generated {factionPowerItems.Count} faction power items: {Path.GetRelativePath(solutionRoot, factionPowerItemsPath)}");
|
||||||
|
|
||||||
|
// Copy item icons next to the markdown docs into the web app's wwwroot so the
|
||||||
|
// pages can reference them. Images are copied, never moved: the docs folder
|
||||||
|
// stays the source of truth.
|
||||||
|
var webImagesRoot = Path.Combine(solutionRoot, "ZeroSpace.Web", "wwwroot", "images");
|
||||||
|
var copiedPlanetaryImages = SyncItemImages(
|
||||||
|
planetaryMasteryItems.Select(static item => (item.ImageFileName, item.ImageSourcePath)),
|
||||||
|
Path.Combine(webImagesRoot, "planetary-mastery-items"),
|
||||||
|
solutionRoot);
|
||||||
|
var copiedGeneralImages = SyncItemImages(
|
||||||
|
generalItems.Select(static item => (item.ImageFileName, item.ImageSourcePath)),
|
||||||
|
Path.Combine(webImagesRoot, "general-items"),
|
||||||
|
solutionRoot);
|
||||||
|
var copiedFactionPowerImages = SyncItemImages(
|
||||||
|
factionPowerItems.Select(static item => (item.ImageFileName, item.ImageSourcePath)),
|
||||||
|
Path.Combine(webImagesRoot, "faction-power-items"),
|
||||||
|
solutionRoot);
|
||||||
|
|
||||||
|
Console.WriteLine($"Copied {copiedPlanetaryImages} planetary mastery item images to {Path.GetRelativePath(solutionRoot, Path.Combine(webImagesRoot, "planetary-mastery-items"))}");
|
||||||
|
Console.WriteLine($"Copied {copiedGeneralImages} general item images to {Path.GetRelativePath(solutionRoot, Path.Combine(webImagesRoot, "general-items"))}");
|
||||||
|
Console.WriteLine($"Copied {copiedFactionPowerImages} faction power item images to {Path.GetRelativePath(solutionRoot, Path.Combine(webImagesRoot, "faction-power-items"))}");
|
||||||
if (skippedDocs.Count > 0)
|
if (skippedDocs.Count > 0)
|
||||||
{
|
{
|
||||||
Console.WriteLine($"Skipped {skippedDocs.Count} incomplete docs:");
|
Console.WriteLine($"Skipped {skippedDocs.Count} incomplete docs:");
|
||||||
@@ -661,6 +730,147 @@ static bool TryReadSystemMastery(
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static bool TryReadPlanetaryMasteryItem(
|
||||||
|
string path,
|
||||||
|
IReadOnlyDictionary<string, FrontMatterValue> frontMatter,
|
||||||
|
string body,
|
||||||
|
HashSet<string> usedIds,
|
||||||
|
out PlanetaryMasteryItemSource item,
|
||||||
|
out string reason)
|
||||||
|
{
|
||||||
|
item = default!;
|
||||||
|
|
||||||
|
var levels = new string[5];
|
||||||
|
for (var level = 1; level <= 5; level++)
|
||||||
|
{
|
||||||
|
if (!TryGetRequiredString(frontMatter, $"level_{level}", out var text, out reason))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
levels[level - 1] = text;
|
||||||
|
}
|
||||||
|
|
||||||
|
var name = Path.GetFileNameWithoutExtension(path);
|
||||||
|
|
||||||
|
// An icon with the same file name as the doc (e.g. "Battery.png" next to
|
||||||
|
// "Battery.md") is treated as this item's image when present.
|
||||||
|
string? imageFileName = null;
|
||||||
|
string? imageSourcePath = null;
|
||||||
|
var candidateImagePath = Path.ChangeExtension(path, ".png");
|
||||||
|
if (File.Exists(candidateImagePath))
|
||||||
|
{
|
||||||
|
imageFileName = Path.GetFileName(candidateImagePath);
|
||||||
|
imageSourcePath = candidateImagePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
item = new PlanetaryMasteryItemSource(
|
||||||
|
ToUniqueIdentifier(name, usedIds),
|
||||||
|
name,
|
||||||
|
levels,
|
||||||
|
imageFileName,
|
||||||
|
imageSourcePath,
|
||||||
|
body);
|
||||||
|
reason = string.Empty;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool TryReadGeneralItem(
|
||||||
|
string path,
|
||||||
|
IReadOnlyDictionary<string, FrontMatterValue> frontMatter,
|
||||||
|
string body,
|
||||||
|
HashSet<string> usedIds,
|
||||||
|
out GeneralItemSource item,
|
||||||
|
out string reason)
|
||||||
|
{
|
||||||
|
item = default!;
|
||||||
|
|
||||||
|
// General items have a variable number of levels (e.g. Renewal Matrix has
|
||||||
|
// two); collect whichever level_1..level_5 entries are present.
|
||||||
|
var levels = ReadOptionalLevels(frontMatter);
|
||||||
|
if (levels.Count == 0)
|
||||||
|
{
|
||||||
|
reason = "missing level_1";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var name = Path.GetFileNameWithoutExtension(path);
|
||||||
|
var (imageFileName, imageSourcePath) = FindItemImage(path);
|
||||||
|
|
||||||
|
item = new GeneralItemSource(
|
||||||
|
ToUniqueIdentifier(name, usedIds),
|
||||||
|
name,
|
||||||
|
levels,
|
||||||
|
TryGetOptionalString(frontMatter, "limitedToMerc"),
|
||||||
|
imageFileName,
|
||||||
|
imageSourcePath,
|
||||||
|
body);
|
||||||
|
reason = string.Empty;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool TryReadFactionPowerItem(
|
||||||
|
string path,
|
||||||
|
IReadOnlyDictionary<string, FrontMatterValue> frontMatter,
|
||||||
|
string body,
|
||||||
|
HashSet<string> usedIds,
|
||||||
|
out FactionPowerItemSource item,
|
||||||
|
out string reason)
|
||||||
|
{
|
||||||
|
item = default!;
|
||||||
|
|
||||||
|
var levels = ReadOptionalLevels(frontMatter);
|
||||||
|
if (levels.Count == 0)
|
||||||
|
{
|
||||||
|
reason = "missing level_1";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var name = Path.GetFileNameWithoutExtension(path);
|
||||||
|
var (imageFileName, imageSourcePath) = FindItemImage(path);
|
||||||
|
|
||||||
|
item = new FactionPowerItemSource(
|
||||||
|
ToUniqueIdentifier(name, usedIds),
|
||||||
|
name,
|
||||||
|
levels,
|
||||||
|
TryGetOptionalString(frontMatter, "limitedTo"),
|
||||||
|
TryGetOptionalString(frontMatter, "limitedToMerc"),
|
||||||
|
TryGetOptionalInt(frontMatter, "minimumLevel"),
|
||||||
|
imageFileName,
|
||||||
|
imageSourcePath,
|
||||||
|
body);
|
||||||
|
reason = string.Empty;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static IReadOnlyList<string> ReadOptionalLevels(IReadOnlyDictionary<string, FrontMatterValue> frontMatter)
|
||||||
|
{
|
||||||
|
var levels = new List<string>();
|
||||||
|
for (var level = 1; level <= 5; level++)
|
||||||
|
{
|
||||||
|
var text = TryGetOptionalString(frontMatter, $"level_{level}");
|
||||||
|
if (!string.IsNullOrWhiteSpace(text))
|
||||||
|
{
|
||||||
|
levels.Add(text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return levels;
|
||||||
|
}
|
||||||
|
|
||||||
|
static (string? FileName, string? SourcePath) FindItemImage(string path)
|
||||||
|
{
|
||||||
|
// An icon with the same file name as the doc (e.g. "Battery.png" next to
|
||||||
|
// "Battery.md") is treated as this item's image when present.
|
||||||
|
var candidateImagePath = Path.ChangeExtension(path, ".png");
|
||||||
|
if (File.Exists(candidateImagePath))
|
||||||
|
{
|
||||||
|
return (Path.GetFileName(candidateImagePath), candidateImagePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (null, null);
|
||||||
|
}
|
||||||
|
|
||||||
static bool IsMasteryField(string key)
|
static bool IsMasteryField(string key)
|
||||||
{
|
{
|
||||||
if (string.Equals(key, "category", StringComparison.OrdinalIgnoreCase) ||
|
if (string.Equals(key, "category", StringComparison.OrdinalIgnoreCase) ||
|
||||||
@@ -766,11 +976,159 @@ static string GenerateSystemMasteries(IReadOnlyList<SystemMasterySource> masteri
|
|||||||
return builder.ToString();
|
return builder.ToString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static string GeneratePlanetaryMasteryItems(IReadOnlyList<PlanetaryMasteryItemSource> items)
|
||||||
|
{
|
||||||
|
var builder = new StringBuilder();
|
||||||
|
builder.AppendLine("// <auto-generated />");
|
||||||
|
builder.AppendLine("namespace ZeroSpace.Model;");
|
||||||
|
builder.AppendLine();
|
||||||
|
builder.AppendLine("public static class PlanetaryMasteryItems");
|
||||||
|
builder.AppendLine("{");
|
||||||
|
|
||||||
|
foreach (var item in items)
|
||||||
|
{
|
||||||
|
builder.AppendLine($" public static readonly PlanetaryMasteryItemData {item.Id} = new(");
|
||||||
|
builder.AppendLine($" Id: {Literal(item.Id)},");
|
||||||
|
builder.AppendLine($" Name: {Literal(item.Name)},");
|
||||||
|
builder.AppendLine($" Levels: [{string.Join(", ", item.Levels.Select(Literal))}],");
|
||||||
|
builder.AppendLine($" ImageFileName: {Literal(item.ImageFileName)},");
|
||||||
|
builder.AppendLine($" Body: {Literal(item.Body)});");
|
||||||
|
builder.AppendLine();
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.AppendLine(" public static IReadOnlyList<PlanetaryMasteryItemData> All { get; } =");
|
||||||
|
builder.AppendLine(" [");
|
||||||
|
|
||||||
|
foreach (var item in items)
|
||||||
|
{
|
||||||
|
builder.AppendLine($" {item.Id},");
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.AppendLine(" ];");
|
||||||
|
builder.AppendLine();
|
||||||
|
builder.AppendLine(" public static IReadOnlyDictionary<string, PlanetaryMasteryItemData> ByName { get; } =");
|
||||||
|
builder.AppendLine(" All.ToDictionary(static item => item.Name, StringComparer.OrdinalIgnoreCase);");
|
||||||
|
builder.AppendLine("}");
|
||||||
|
|
||||||
|
return builder.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
static string GenerateGeneralItems(IReadOnlyList<GeneralItemSource> items)
|
||||||
|
{
|
||||||
|
var builder = new StringBuilder();
|
||||||
|
builder.AppendLine("// <auto-generated />");
|
||||||
|
builder.AppendLine("namespace ZeroSpace.Model;");
|
||||||
|
builder.AppendLine();
|
||||||
|
builder.AppendLine("public static class GeneralItems");
|
||||||
|
builder.AppendLine("{");
|
||||||
|
|
||||||
|
foreach (var item in items)
|
||||||
|
{
|
||||||
|
builder.AppendLine($" public static readonly GeneralItemData {item.Id} = new(");
|
||||||
|
builder.AppendLine($" Id: {Literal(item.Id)},");
|
||||||
|
builder.AppendLine($" Name: {Literal(item.Name)},");
|
||||||
|
builder.AppendLine($" Levels: [{string.Join(", ", item.Levels.Select(Literal))}],");
|
||||||
|
builder.AppendLine($" LimitedToMerc: {Literal(item.LimitedToMerc)},");
|
||||||
|
builder.AppendLine($" ImageFileName: {Literal(item.ImageFileName)},");
|
||||||
|
builder.AppendLine($" Body: {Literal(item.Body)});");
|
||||||
|
builder.AppendLine();
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.AppendLine(" public static IReadOnlyList<GeneralItemData> All { get; } =");
|
||||||
|
builder.AppendLine(" [");
|
||||||
|
|
||||||
|
foreach (var item in items)
|
||||||
|
{
|
||||||
|
builder.AppendLine($" {item.Id},");
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.AppendLine(" ];");
|
||||||
|
builder.AppendLine();
|
||||||
|
builder.AppendLine(" public static IReadOnlyDictionary<string, GeneralItemData> ByName { get; } =");
|
||||||
|
builder.AppendLine(" All.ToDictionary(static item => item.Name, StringComparer.OrdinalIgnoreCase);");
|
||||||
|
builder.AppendLine("}");
|
||||||
|
|
||||||
|
return builder.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
static string GenerateFactionPowerItems(IReadOnlyList<FactionPowerItemSource> items)
|
||||||
|
{
|
||||||
|
var builder = new StringBuilder();
|
||||||
|
builder.AppendLine("// <auto-generated />");
|
||||||
|
builder.AppendLine("namespace ZeroSpace.Model;");
|
||||||
|
builder.AppendLine();
|
||||||
|
builder.AppendLine("public static class FactionPowerItems");
|
||||||
|
builder.AppendLine("{");
|
||||||
|
|
||||||
|
foreach (var item in items)
|
||||||
|
{
|
||||||
|
builder.AppendLine($" public static readonly FactionPowerItemData {item.Id} = new(");
|
||||||
|
builder.AppendLine($" Id: {Literal(item.Id)},");
|
||||||
|
builder.AppendLine($" Name: {Literal(item.Name)},");
|
||||||
|
builder.AppendLine($" Levels: [{string.Join(", ", item.Levels.Select(Literal))}],");
|
||||||
|
builder.AppendLine($" LimitedTo: {Literal(item.LimitedTo)},");
|
||||||
|
builder.AppendLine($" LimitedToMerc: {Literal(item.LimitedToMerc)},");
|
||||||
|
builder.AppendLine($" MinimumLevel: {item.MinimumLevel?.ToString(CultureInfo.InvariantCulture) ?? "null"},");
|
||||||
|
builder.AppendLine($" ImageFileName: {Literal(item.ImageFileName)},");
|
||||||
|
builder.AppendLine($" Body: {Literal(item.Body)});");
|
||||||
|
builder.AppendLine();
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.AppendLine(" public static IReadOnlyList<FactionPowerItemData> All { get; } =");
|
||||||
|
builder.AppendLine(" [");
|
||||||
|
|
||||||
|
foreach (var item in items)
|
||||||
|
{
|
||||||
|
builder.AppendLine($" {item.Id},");
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.AppendLine(" ];");
|
||||||
|
builder.AppendLine();
|
||||||
|
builder.AppendLine(" public static IReadOnlyDictionary<string, FactionPowerItemData> ByName { get; } =");
|
||||||
|
builder.AppendLine(" All.ToDictionary(static item => item.Name, StringComparer.OrdinalIgnoreCase);");
|
||||||
|
builder.AppendLine("}");
|
||||||
|
|
||||||
|
return builder.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
static string BoolLiteral(bool value)
|
static string BoolLiteral(bool value)
|
||||||
{
|
{
|
||||||
return value ? "true" : "false";
|
return value ? "true" : "false";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static int SyncItemImages(
|
||||||
|
IEnumerable<(string? FileName, string? SourcePath)> items,
|
||||||
|
string imagesDir,
|
||||||
|
string solutionRoot)
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(imagesDir);
|
||||||
|
|
||||||
|
var expectedImages = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
var copiedImages = 0;
|
||||||
|
foreach (var (fileName, sourcePath) in items)
|
||||||
|
{
|
||||||
|
if (fileName is null || sourcePath is null)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
expectedImages.Add(fileName);
|
||||||
|
File.Copy(sourcePath, Path.Combine(imagesDir, fileName), overwrite: true);
|
||||||
|
copiedImages++;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var stale in Directory.EnumerateFiles(imagesDir, "*.png"))
|
||||||
|
{
|
||||||
|
if (!expectedImages.Contains(Path.GetFileName(stale)))
|
||||||
|
{
|
||||||
|
File.Delete(stale);
|
||||||
|
Console.WriteLine($"Removed stale item image: {Path.GetRelativePath(solutionRoot, stale)}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return copiedImages;
|
||||||
|
}
|
||||||
|
|
||||||
static double GetDouble(IReadOnlyDictionary<string, FrontMatterValue> frontMatter, string key)
|
static double GetDouble(IReadOnlyDictionary<string, FrontMatterValue> frontMatter, string key)
|
||||||
{
|
{
|
||||||
if (frontMatter.TryGetValue(key, out var value) &&
|
if (frontMatter.TryGetValue(key, out var value) &&
|
||||||
@@ -833,3 +1191,31 @@ internal sealed record SystemMasterySource(
|
|||||||
IReadOnlyList<string> AvailableSystems,
|
IReadOnlyList<string> AvailableSystems,
|
||||||
IReadOnlyList<(string Key, double Value)> Stats,
|
IReadOnlyList<(string Key, double Value)> Stats,
|
||||||
string Body);
|
string Body);
|
||||||
|
|
||||||
|
internal sealed record PlanetaryMasteryItemSource(
|
||||||
|
string Id,
|
||||||
|
string Name,
|
||||||
|
IReadOnlyList<string> Levels,
|
||||||
|
string? ImageFileName,
|
||||||
|
string? ImageSourcePath,
|
||||||
|
string Body);
|
||||||
|
|
||||||
|
internal sealed record GeneralItemSource(
|
||||||
|
string Id,
|
||||||
|
string Name,
|
||||||
|
IReadOnlyList<string> Levels,
|
||||||
|
string? LimitedToMerc,
|
||||||
|
string? ImageFileName,
|
||||||
|
string? ImageSourcePath,
|
||||||
|
string Body);
|
||||||
|
|
||||||
|
internal sealed record FactionPowerItemSource(
|
||||||
|
string Id,
|
||||||
|
string Name,
|
||||||
|
IReadOnlyList<string> Levels,
|
||||||
|
string? LimitedTo,
|
||||||
|
string? LimitedToMerc,
|
||||||
|
int? MinimumLevel,
|
||||||
|
string? ImageFileName,
|
||||||
|
string? ImageSourcePath,
|
||||||
|
string Body);
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
namespace ZeroSpace.Model;
|
||||||
|
|
||||||
|
public sealed record FactionPowerItemData(
|
||||||
|
string Id,
|
||||||
|
string Name,
|
||||||
|
IReadOnlyList<string> Levels,
|
||||||
|
string? LimitedTo,
|
||||||
|
string? LimitedToMerc,
|
||||||
|
int? MinimumLevel,
|
||||||
|
string? ImageFileName,
|
||||||
|
string Body);
|
||||||
@@ -0,0 +1,199 @@
|
|||||||
|
// <auto-generated />
|
||||||
|
namespace ZeroSpace.Model;
|
||||||
|
|
||||||
|
public static class FactionPowerItems
|
||||||
|
{
|
||||||
|
public static readonly FactionPowerItemData Airstrike = new(
|
||||||
|
Id: "Airstrike",
|
||||||
|
Name: "Airstrike",
|
||||||
|
Levels: ["Calls down an Airstrike, dealing 300 damage to ground units. Requires Airstrips."],
|
||||||
|
LimitedTo: "Prefect Aster",
|
||||||
|
LimitedToMerc: null,
|
||||||
|
MinimumLevel: 2,
|
||||||
|
ImageFileName: "Airstrike.png",
|
||||||
|
Body: "");
|
||||||
|
|
||||||
|
public static readonly FactionPowerItemData Destroyer = new(
|
||||||
|
Id: "Destroyer",
|
||||||
|
Name: "Destroyer",
|
||||||
|
Levels: ["Spawns a powerful guardian with limited life. Spawns Boomers over time and on death. Retains infused when called again."],
|
||||||
|
LimitedTo: "Vynthra",
|
||||||
|
LimitedToMerc: null,
|
||||||
|
MinimumLevel: 10,
|
||||||
|
ImageFileName: "Destroyer.png",
|
||||||
|
Body: "");
|
||||||
|
|
||||||
|
public static readonly FactionPowerItemData ForcedAssimilation = new(
|
||||||
|
Id: "ForcedAssimilation",
|
||||||
|
Name: "Forced Assimilation",
|
||||||
|
Levels: ["Target a friendly T1/ T2 unit. All nearby friendly T1/T2 units are assimilated, giving 30% of their health, shield and damage to the targeted unit, and an additional 20% if the unit is a T1 unit. Cannot be used on the same unit more than once."],
|
||||||
|
LimitedTo: "Anrubic",
|
||||||
|
LimitedToMerc: null,
|
||||||
|
MinimumLevel: 1,
|
||||||
|
ImageFileName: "Forced Assimilation.png",
|
||||||
|
Body: "");
|
||||||
|
|
||||||
|
public static readonly FactionPowerItemData Havoc = new(
|
||||||
|
Id: "Havoc",
|
||||||
|
Name: "Havoc",
|
||||||
|
Levels: ["Spawn a Havoc at location. It charges through enemies. Explodes on death dealing 300 damage."],
|
||||||
|
LimitedTo: "Anrubic",
|
||||||
|
LimitedToMerc: null,
|
||||||
|
MinimumLevel: 1,
|
||||||
|
ImageFileName: "Havoc.png",
|
||||||
|
Body: "");
|
||||||
|
|
||||||
|
public static readonly FactionPowerItemData HealingPod = new(
|
||||||
|
Id: "HealingPod",
|
||||||
|
Name: "Healing Pod",
|
||||||
|
Levels: ["Spawn a Nourishing Pod and three Seedlings. Nourishing Pod heals nearby units."],
|
||||||
|
LimitedTo: "Vynthra",
|
||||||
|
LimitedToMerc: null,
|
||||||
|
MinimumLevel: 1,
|
||||||
|
ImageFileName: "Healing Pod.png",
|
||||||
|
Body: "");
|
||||||
|
|
||||||
|
public static readonly FactionPowerItemData Infusion = new(
|
||||||
|
Id: "Infusion",
|
||||||
|
Name: "Infusion",
|
||||||
|
Levels: ["Increases the target's health, attack damage, health and energy regeneration."],
|
||||||
|
LimitedTo: "Vynthra",
|
||||||
|
LimitedToMerc: null,
|
||||||
|
MinimumLevel: 1,
|
||||||
|
ImageFileName: "Infusion.png",
|
||||||
|
Body: "");
|
||||||
|
|
||||||
|
public static readonly FactionPowerItemData Minefield = new(
|
||||||
|
Id: "Minefield",
|
||||||
|
Name: "Minefield",
|
||||||
|
Levels: ["Spawn 12 mines that damage and stun enemies", "Spawn 14 mines that damage and stun enemies", "Spawn 16 mines that damage and stun enemies", "Spawn 18 mines that damage and stun enemies", "Spawn 20 mines that damage and stun enemies"],
|
||||||
|
LimitedTo: null,
|
||||||
|
LimitedToMerc: "Free Corsair",
|
||||||
|
MinimumLevel: null,
|
||||||
|
ImageFileName: "Minefield.png",
|
||||||
|
Body: "");
|
||||||
|
|
||||||
|
public static readonly FactionPowerItemData NanoSwarm = new(
|
||||||
|
Id: "NanoSwarm",
|
||||||
|
Name: "Nano Swarm",
|
||||||
|
Levels: ["Spawn a Nano Swarm at targeted location. It draws enemy fire. After 15 seconds it explodes dealing damage to enemies. The damage increases when it takes damage."],
|
||||||
|
LimitedTo: "Anrubic",
|
||||||
|
LimitedToMerc: null,
|
||||||
|
MinimumLevel: 9,
|
||||||
|
ImageFileName: "Nano Swarm.png",
|
||||||
|
Body: "");
|
||||||
|
|
||||||
|
public static readonly FactionPowerItemData OrbitalLaser = new(
|
||||||
|
Id: "OrbitalLaser",
|
||||||
|
Name: "Orbital Laser",
|
||||||
|
Levels: ["Heals friendly units and Revives recently lost units."],
|
||||||
|
LimitedTo: null,
|
||||||
|
LimitedToMerc: "The Molten",
|
||||||
|
MinimumLevel: 3,
|
||||||
|
ImageFileName: "Orbital Laser.png",
|
||||||
|
Body: "");
|
||||||
|
|
||||||
|
public static readonly FactionPowerItemData Reinforcements = new(
|
||||||
|
Id: "Reinforcements",
|
||||||
|
Name: "Reinforcements",
|
||||||
|
Levels: ["Call down reinforcements of the selected type."],
|
||||||
|
LimitedTo: "Prefect Aster",
|
||||||
|
LimitedToMerc: null,
|
||||||
|
MinimumLevel: 1,
|
||||||
|
ImageFileName: "Reinforcements.png",
|
||||||
|
Body: "");
|
||||||
|
|
||||||
|
public static readonly FactionPowerItemData Replicate = new(
|
||||||
|
Id: "Replicate",
|
||||||
|
Name: "Replicate",
|
||||||
|
Levels: ["Create copies of the targeted friendly units."],
|
||||||
|
LimitedTo: "Anrubic",
|
||||||
|
LimitedToMerc: null,
|
||||||
|
MinimumLevel: 2,
|
||||||
|
ImageFileName: "Replicate.png",
|
||||||
|
Body: "");
|
||||||
|
|
||||||
|
public static readonly FactionPowerItemData Shockwave = new(
|
||||||
|
Id: "Shockwave",
|
||||||
|
Name: "Shockwave",
|
||||||
|
Levels: ["Stuns enemy units in area for 3 seconds."],
|
||||||
|
LimitedTo: "Prefect Aster",
|
||||||
|
LimitedToMerc: null,
|
||||||
|
MinimumLevel: 1,
|
||||||
|
ImageFileName: "Shockwave.png",
|
||||||
|
Body: "");
|
||||||
|
|
||||||
|
public static readonly FactionPowerItemData SolInvictus = new(
|
||||||
|
Id: "SolInvictus",
|
||||||
|
Name: "Sol Invictus",
|
||||||
|
Levels: ["Call down a powerful mech that explodes on death and sets the ground on fire."],
|
||||||
|
LimitedTo: "Prefect Aster",
|
||||||
|
LimitedToMerc: null,
|
||||||
|
MinimumLevel: 7,
|
||||||
|
ImageFileName: "Sol Invictus.png",
|
||||||
|
Body: "");
|
||||||
|
|
||||||
|
public static readonly FactionPowerItemData SpawnMonolith = new(
|
||||||
|
Id: "SpawnMonolith",
|
||||||
|
Name: "Spawn Monolith",
|
||||||
|
Levels: ["Summon a powerful structure. Nearby Thrall deaths cause bonus attacks to occur."],
|
||||||
|
LimitedTo: "Galavax",
|
||||||
|
LimitedToMerc: null,
|
||||||
|
MinimumLevel: 7,
|
||||||
|
ImageFileName: "Spawn Monolith.png",
|
||||||
|
Body: "");
|
||||||
|
|
||||||
|
public static readonly FactionPowerItemData SpawnObelisks = new(
|
||||||
|
Id: "SpawnObelisks",
|
||||||
|
Name: "Spawn Obelisks",
|
||||||
|
Levels: ["Spawn various obelisks on the battlefield"],
|
||||||
|
LimitedTo: "Galavax",
|
||||||
|
LimitedToMerc: null,
|
||||||
|
MinimumLevel: 1,
|
||||||
|
ImageFileName: "Spawn Obelisks.png",
|
||||||
|
Body: "");
|
||||||
|
|
||||||
|
public static readonly FactionPowerItemData SummonEmperor = new(
|
||||||
|
Id: "SummonEmperor",
|
||||||
|
Name: "Summon Emperor",
|
||||||
|
Levels: ["Summoning takes 15 durartion. Units dying nearby the site empower the Emperor. Thralls can be sacrificed with 75% cost refund (right-click on the site). Emperor loses health when summoned."],
|
||||||
|
LimitedTo: "Galavax",
|
||||||
|
LimitedToMerc: null,
|
||||||
|
MinimumLevel: 1,
|
||||||
|
ImageFileName: "Summon Emperor.png",
|
||||||
|
Body: "");
|
||||||
|
|
||||||
|
public static readonly FactionPowerItemData TreeOfLife = new(
|
||||||
|
Id: "TreeOfLife",
|
||||||
|
Name: "Tree of Life",
|
||||||
|
Levels: ["Heals friendly units and Revives recently lost units."],
|
||||||
|
LimitedTo: "Vynthra",
|
||||||
|
LimitedToMerc: null,
|
||||||
|
MinimumLevel: 3,
|
||||||
|
ImageFileName: "Tree of Life.png",
|
||||||
|
Body: "");
|
||||||
|
|
||||||
|
public static IReadOnlyList<FactionPowerItemData> All { get; } =
|
||||||
|
[
|
||||||
|
Airstrike,
|
||||||
|
Destroyer,
|
||||||
|
ForcedAssimilation,
|
||||||
|
Havoc,
|
||||||
|
HealingPod,
|
||||||
|
Infusion,
|
||||||
|
Minefield,
|
||||||
|
NanoSwarm,
|
||||||
|
OrbitalLaser,
|
||||||
|
Reinforcements,
|
||||||
|
Replicate,
|
||||||
|
Shockwave,
|
||||||
|
SolInvictus,
|
||||||
|
SpawnMonolith,
|
||||||
|
SpawnObelisks,
|
||||||
|
SummonEmperor,
|
||||||
|
TreeOfLife,
|
||||||
|
];
|
||||||
|
|
||||||
|
public static IReadOnlyDictionary<string, FactionPowerItemData> ByName { get; } =
|
||||||
|
All.ToDictionary(static item => item.Name, StringComparer.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
namespace ZeroSpace.Model;
|
||||||
|
|
||||||
|
public sealed record GeneralItemData(
|
||||||
|
string Id,
|
||||||
|
string Name,
|
||||||
|
IReadOnlyList<string> Levels,
|
||||||
|
string? LimitedToMerc,
|
||||||
|
string? ImageFileName,
|
||||||
|
string Body);
|
||||||
@@ -0,0 +1,210 @@
|
|||||||
|
// <auto-generated />
|
||||||
|
namespace ZeroSpace.Model;
|
||||||
|
|
||||||
|
public static class GeneralItems
|
||||||
|
{
|
||||||
|
public static readonly GeneralItemData ArandiPenumbra = new(
|
||||||
|
Id: "ArandiPenumbra",
|
||||||
|
Name: "Arandi Penumbra",
|
||||||
|
Levels: ["Unlocks construction of Arandi Penumbra and production of mercenaries from it.", "Increase mercenary health +15% and damage +15%", "Increase mercenary compound limit by 1.", "Increase mercenary health +30% and damage +30%", "Increase mercenary compound limit by 1."],
|
||||||
|
LimitedToMerc: "Arandi",
|
||||||
|
ImageFileName: "Arandi Penumbra.png",
|
||||||
|
Body: "");
|
||||||
|
|
||||||
|
public static readonly GeneralItemData AssaultforgedWeaponry = new(
|
||||||
|
Id: "AssaultforgedWeaponry",
|
||||||
|
Name: "Assaultforged Weaponry",
|
||||||
|
Levels: ["+20% weapon damage for melee units", "+25% weapon damage for melee units", "+30% weapon damage for melee units", "+35% weapon damage for melee units", "+40% weapon damage for melee units"],
|
||||||
|
LimitedToMerc: "Cha'Kru Kingdom",
|
||||||
|
ImageFileName: "Assaultforged Weaponry.png",
|
||||||
|
Body: "");
|
||||||
|
|
||||||
|
public static readonly GeneralItemData ElderbastionShell = new(
|
||||||
|
Id: "ElderbastionShell",
|
||||||
|
Name: "Elderbastion Shell",
|
||||||
|
Levels: ["Gives you access to the wall building. +15% vitals for buildings.", "+30% vitals for buildings.", "+45% vitals for buildings.", "+60% vitals for buildings.", "+75% vitals for buildings."],
|
||||||
|
LimitedToMerc: null,
|
||||||
|
ImageFileName: "Elderbastion Shell.png",
|
||||||
|
Body: "");
|
||||||
|
|
||||||
|
public static readonly GeneralItemData EnigmaticCapacitor = new(
|
||||||
|
Id: "EnigmaticCapacitor",
|
||||||
|
Name: "Enigmatic Capacitor",
|
||||||
|
Levels: ["-5% unit ability cooldowns.", "-10% unit ability cooldowns.", "-15% unit ability cooldowns.", "-20% unit ability cooldowns.", "-25% unit ability cooldowns."],
|
||||||
|
LimitedToMerc: null,
|
||||||
|
ImageFileName: "Enigmatic Capacitor.png",
|
||||||
|
Body: "");
|
||||||
|
|
||||||
|
public static readonly GeneralItemData FreeCorsairMercenaryOutpost = new(
|
||||||
|
Id: "FreeCorsairMercenaryOutpost",
|
||||||
|
Name: "Free Corsair Mercenary Outpost",
|
||||||
|
Levels: ["Unlocks construction of Free Corsair Mercernary Outpost and production of mercenaries from it.", "Increase mercenary health +15% and damage +15%", "Increase mercenary compound limit by 1.", "Increase mercenary health +30% and damage +30%", "Increase mercenary compound limit by 1."],
|
||||||
|
LimitedToMerc: "Free Corsair",
|
||||||
|
ImageFileName: "Free Corsair Mercenary Outpost.png",
|
||||||
|
Body: "");
|
||||||
|
|
||||||
|
public static readonly GeneralItemData GensisCore = new(
|
||||||
|
Id: "GensisCore",
|
||||||
|
Name: "Gensis Core",
|
||||||
|
Levels: ["+1 unit health regeneration per second.", "+2 unit health regeneration per second.", "+3 unit health regeneration per second.", "+4 unit health regeneration per second.", "+5 unit health regeneration per second."],
|
||||||
|
LimitedToMerc: null,
|
||||||
|
ImageFileName: "Gensis Core.png",
|
||||||
|
Body: "");
|
||||||
|
|
||||||
|
public static readonly GeneralItemData Heroesbane = new(
|
||||||
|
Id: "Heroesbane",
|
||||||
|
Name: "Heroesbane",
|
||||||
|
Levels: ["+40% lifesteal to heroic units", "+45% lifesteal to heroic units", "+50% lifesteal to heroic units", "+55% lifesteal to heroic units", "+60% lifesteal to heroic units"],
|
||||||
|
LimitedToMerc: "Valkaru",
|
||||||
|
ImageFileName: "Heroesbane.png",
|
||||||
|
Body: "");
|
||||||
|
|
||||||
|
public static readonly GeneralItemData KaikinoLifebleeders = new(
|
||||||
|
Id: "KaikinoLifebleeders",
|
||||||
|
Name: "Kaikino Lifebleeders",
|
||||||
|
Levels: ["+30% lifesteal for melee units", "+35% lifesteal for melee units", "+40% lifesteal for melee units", "+45% lifesteal for melee units", "+50% lifesteal for melee units"],
|
||||||
|
LimitedToMerc: "Endari",
|
||||||
|
ImageFileName: "Kaikino Lifebleeders.png",
|
||||||
|
Body: "");
|
||||||
|
|
||||||
|
public static readonly GeneralItemData LightweightAlloys = new(
|
||||||
|
Id: "LightweightAlloys",
|
||||||
|
Name: "Lightweight alloys",
|
||||||
|
Levels: ["+10% unit movement speed.", "+20% unit movement speed.", "+30% unit movement speed.", "+40% unit movement speed.", "+50% unit movement speed."],
|
||||||
|
LimitedToMerc: null,
|
||||||
|
ImageFileName: "Lightweight alloys.png",
|
||||||
|
Body: "");
|
||||||
|
|
||||||
|
public static readonly GeneralItemData MarranBulwark = new(
|
||||||
|
Id: "MarranBulwark",
|
||||||
|
Name: "Marran Bulwark",
|
||||||
|
Levels: ["Unlocks construction of Marran Bulwark and production of mercenaries from it.", "Increase mercenary health +15% and damage +15%", "Increase mercenary compound limit by 1.", "Increase mercenary health +30% and damage +30%", "Increase mercenary compound limit by 1."],
|
||||||
|
LimitedToMerc: "Marran",
|
||||||
|
ImageFileName: "Marran Bulwark.png",
|
||||||
|
Body: "");
|
||||||
|
|
||||||
|
public static readonly GeneralItemData MirageArmour = new(
|
||||||
|
Id: "MirageArmour",
|
||||||
|
Name: "Mirage Armour",
|
||||||
|
Levels: ["Units take 5% less damage.", "Units take 7.5% less damage.", "Units take 10% less damage.", "Units take 12.5% less damage.", "Units take 15% less damage."],
|
||||||
|
LimitedToMerc: "Arandi",
|
||||||
|
ImageFileName: "Mirage Armour.png",
|
||||||
|
Body: "");
|
||||||
|
|
||||||
|
public static readonly GeneralItemData RemnantEngineering = new(
|
||||||
|
Id: "RemnantEngineering",
|
||||||
|
Name: "Remnant Engineering",
|
||||||
|
Levels: ["+25% production, research and construction speed.", "+30% production, research and construction speed.", "+35% production, research and construction speed.", "+40% production, research and construction speed.", "+50% production, research and construction speed."],
|
||||||
|
LimitedToMerc: null,
|
||||||
|
ImageFileName: "Remnant Engineering.png",
|
||||||
|
Body: "");
|
||||||
|
|
||||||
|
public static readonly GeneralItemData RenewalMatrix = new(
|
||||||
|
Id: "RenewalMatrix",
|
||||||
|
Name: "Renewal Matrix",
|
||||||
|
Levels: ["5x faster hero and harvester respawn rate.", "10x faster hero and harvester respawn rate."],
|
||||||
|
LimitedToMerc: null,
|
||||||
|
ImageFileName: "Renewal Matrix.png",
|
||||||
|
Body: "");
|
||||||
|
|
||||||
|
public static readonly GeneralItemData Scavenging = new(
|
||||||
|
Id: "Scavenging",
|
||||||
|
Name: "Scavenging",
|
||||||
|
Levels: ["Gain 4% enemy structures cost as flux after destroying it", "Gain 6% enemy structures cost as flux after destroying it", "Gain 8% enemy structures cost as flux after destroying it", "Gain 10% enemy structures cost as flux after destroying it", "Gain 12% enemy structures cost as flux after destroying it"],
|
||||||
|
LimitedToMerc: "Free Corsair",
|
||||||
|
ImageFileName: "Scavenging.png",
|
||||||
|
Body: "");
|
||||||
|
|
||||||
|
public static readonly GeneralItemData SilentHollow = new(
|
||||||
|
Id: "SilentHollow",
|
||||||
|
Name: "Silent Hollow",
|
||||||
|
Levels: ["Unlocks construction of Deep One and production of mercenaries from it.", "Increase mercenary health +15% and damage +15%", "Increase mercenary compound limit by 1.", "Increase mercenary health +30% and damage +30%", "Increase mercenary compound limit by 1."],
|
||||||
|
LimitedToMerc: "Enrandi",
|
||||||
|
ImageFileName: "Silent Hollow.png",
|
||||||
|
Body: "");
|
||||||
|
|
||||||
|
public static readonly GeneralItemData SingularityCell = new(
|
||||||
|
Id: "SingularityCell",
|
||||||
|
Name: "Singularity Cell",
|
||||||
|
Levels: ["+20% unit energy regeneration.", "+40% unit energy regeneration.", "+60% unit energy regeneration.", "+80% unit energy regeneration.", "+100% unit energy regeneration."],
|
||||||
|
LimitedToMerc: null,
|
||||||
|
ImageFileName: "Singularity Cell.png",
|
||||||
|
Body: "");
|
||||||
|
|
||||||
|
public static readonly GeneralItemData StarhunterScopes = new(
|
||||||
|
Id: "StarhunterScopes",
|
||||||
|
Name: "Starhunter Scopes",
|
||||||
|
Levels: ["+0.5 attack range to army units", "+0.75 attack range to army units", "+1 attack range to army units", "+1.25 attack range to army units", "+1.5 attack range to army units"],
|
||||||
|
LimitedToMerc: null,
|
||||||
|
ImageFileName: "Starhunter Scopes.png",
|
||||||
|
Body: "");
|
||||||
|
|
||||||
|
public static readonly GeneralItemData SupplyCache = new(
|
||||||
|
Id: "SupplyCache",
|
||||||
|
Name: "Supply Cache",
|
||||||
|
Levels: ["+20 starting and max supply.", "+25 starting and max supply.", "+30 starting and max supply.", "+35 starting and max supply.", "+40 starting and max supply."],
|
||||||
|
LimitedToMerc: null,
|
||||||
|
ImageFileName: "Supply Cache.png",
|
||||||
|
Body: "");
|
||||||
|
|
||||||
|
public static readonly GeneralItemData TempleOfAnqas = new(
|
||||||
|
Id: "TempleOfAnqas",
|
||||||
|
Name: "Temple of Anqas",
|
||||||
|
Levels: ["Unlocks construction of Cha'kru Temple of Anqas and production of mercenaries from it.", "Increase mercenary health +15% and damage +15%", "Increase mercenary compound limit by 1.", "Increase mercenary health +30% and damage +30%", "Increase mercenary compound limit by 1."],
|
||||||
|
LimitedToMerc: "Cha'Kru Kingdom",
|
||||||
|
ImageFileName: "Temple of Anqas.png",
|
||||||
|
Body: "");
|
||||||
|
|
||||||
|
public static readonly GeneralItemData TempleOfStars = new(
|
||||||
|
Id: "TempleOfStars",
|
||||||
|
Name: "Temple of Stars",
|
||||||
|
Levels: ["Unlocks construction of Cha'kru Temple of Stars and production of mercenaries from it.", "Increase mercenary health +15% and damage +15%", "Increase mercenary compound limit by 1.", "Increase mercenary health +30% and damage +30%", "Increase mercenary compound limit by 1."],
|
||||||
|
LimitedToMerc: null,
|
||||||
|
ImageFileName: "Temple of Stars.png",
|
||||||
|
Body: "");
|
||||||
|
|
||||||
|
public static readonly GeneralItemData ValkaruWarVault = new(
|
||||||
|
Id: "ValkaruWarVault",
|
||||||
|
Name: "Valkaru War Vault",
|
||||||
|
Levels: ["Unlocks construction of Valkaru War Vault and production of mercenaries from it.", "Increase mercenary health +15% and damage +15%", "Increase mercenary compound limit by 1.", "Increase mercenary health +30% and damage +30%", "Increase mercenary compound limit by 1."],
|
||||||
|
LimitedToMerc: "Valkaru",
|
||||||
|
ImageFileName: "Valkaru War Vault.png",
|
||||||
|
Body: "");
|
||||||
|
|
||||||
|
public static readonly GeneralItemData XolRelicOutpost = new(
|
||||||
|
Id: "XolRelicOutpost",
|
||||||
|
Name: "Xol Relic Outpost",
|
||||||
|
Levels: ["Ancient Xol Master relic. Calls Xol mercenaries to fight for you.", "Unlock Molten Xyther", "Unlock Molten Synapse", "Unlock Molten Aegis", "Can build additional Xol Relic Outpost"],
|
||||||
|
LimitedToMerc: null,
|
||||||
|
ImageFileName: "Xol Relic Outpost.png",
|
||||||
|
Body: "");
|
||||||
|
|
||||||
|
public static IReadOnlyList<GeneralItemData> All { get; } =
|
||||||
|
[
|
||||||
|
ArandiPenumbra,
|
||||||
|
AssaultforgedWeaponry,
|
||||||
|
ElderbastionShell,
|
||||||
|
EnigmaticCapacitor,
|
||||||
|
FreeCorsairMercenaryOutpost,
|
||||||
|
GensisCore,
|
||||||
|
Heroesbane,
|
||||||
|
KaikinoLifebleeders,
|
||||||
|
LightweightAlloys,
|
||||||
|
MarranBulwark,
|
||||||
|
MirageArmour,
|
||||||
|
RemnantEngineering,
|
||||||
|
RenewalMatrix,
|
||||||
|
Scavenging,
|
||||||
|
SilentHollow,
|
||||||
|
SingularityCell,
|
||||||
|
StarhunterScopes,
|
||||||
|
SupplyCache,
|
||||||
|
TempleOfAnqas,
|
||||||
|
TempleOfStars,
|
||||||
|
ValkaruWarVault,
|
||||||
|
XolRelicOutpost,
|
||||||
|
];
|
||||||
|
|
||||||
|
public static IReadOnlyDictionary<string, GeneralItemData> ByName { get; } =
|
||||||
|
All.ToDictionary(static item => item.Name, StringComparer.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
namespace ZeroSpace.Model;
|
||||||
|
|
||||||
|
public sealed record PlanetaryMasteryItemData(
|
||||||
|
string Id,
|
||||||
|
string Name,
|
||||||
|
IReadOnlyList<string> Levels,
|
||||||
|
string? ImageFileName,
|
||||||
|
string Body);
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
// <auto-generated />
|
||||||
|
namespace ZeroSpace.Model;
|
||||||
|
|
||||||
|
public static class PlanetaryMasteryItems
|
||||||
|
{
|
||||||
|
public static readonly PlanetaryMasteryItemData Accelerant = new(
|
||||||
|
Id: "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."],
|
||||||
|
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.");
|
||||||
|
|
||||||
|
public static readonly PlanetaryMasteryItemData Battery = new(
|
||||||
|
Id: "Battery",
|
||||||
|
Name: "Battery",
|
||||||
|
Levels: ["+10 starting topbar energy.", "+12.5 starting topbar energy.", "+15 starting topbar energy.", "+17.5 starting topbar energy.", "+20 starting topbar energy."],
|
||||||
|
ImageFileName: "Battery.png",
|
||||||
|
Body: "");
|
||||||
|
|
||||||
|
public static readonly PlanetaryMasteryItemData EnergySiphon = new(
|
||||||
|
Id: "EnergySiphon",
|
||||||
|
Name: "Energy Siphon",
|
||||||
|
Levels: ["+0.15 Topbar regeneration rate.", "+0.2 Topbar regeneration rate.", "+0.25 Topbar regeneration rate.", "+0.3 Topbar regeneration rate.", "+0.35 Topbar regeneration rate."],
|
||||||
|
ImageFileName: "Energy Siphon.png",
|
||||||
|
Body: "");
|
||||||
|
|
||||||
|
public static readonly PlanetaryMasteryItemData FasterReload = new(
|
||||||
|
Id: "FasterReload",
|
||||||
|
Name: "Faster Reload",
|
||||||
|
Levels: ["Units attack +10% faster.", "Units attack +12.5% faster.", "Units attack +15% faster.", "Units attack +17.5% faster.", "Units attack +20% faster."],
|
||||||
|
ImageFileName: "Faster Reload.png",
|
||||||
|
Body: "");
|
||||||
|
|
||||||
|
public static readonly PlanetaryMasteryItemData FluxCache = new(
|
||||||
|
Id: "FluxCache",
|
||||||
|
Name: "Flux Cache",
|
||||||
|
Levels: ["+50 Starting Flux", "+75 Starting Flux", "+100 Starting Flux", "+125 Starting Flux", "+150 Starting Flux"],
|
||||||
|
ImageFileName: "Flux Cache.png",
|
||||||
|
Body: "");
|
||||||
|
|
||||||
|
public static readonly PlanetaryMasteryItemData GenesisPlating = new(
|
||||||
|
Id: "GenesisPlating",
|
||||||
|
Name: "Genesis Plating",
|
||||||
|
Levels: ["+10% unit health and shields.", "+12.5% unit health and shields.", "+15% unit health and shields.", "+17.5% unit health and shields.", "+20% unit health and shields."],
|
||||||
|
ImageFileName: "Genesis Plating.png",
|
||||||
|
Body: "");
|
||||||
|
|
||||||
|
public static readonly PlanetaryMasteryItemData HexiteCache = new(
|
||||||
|
Id: "HexiteCache",
|
||||||
|
Name: "Hexite Cache",
|
||||||
|
Levels: ["+100 Starting Hexite", "+125 Starting Hexite", "+150 Starting Hexite", "+175 Starting Hexite", "+200 Starting Hexite"],
|
||||||
|
ImageFileName: "Hexite Cache.png",
|
||||||
|
Body: "");
|
||||||
|
|
||||||
|
public static readonly PlanetaryMasteryItemData ImprovedWeaponry = new(
|
||||||
|
Id: "ImprovedWeaponry",
|
||||||
|
Name: "Improved Weaponry",
|
||||||
|
Levels: ["+10% unit weapon damage.", "+12.5% unit weapon damage.", "+15% unit weapon damage.", "+17.5% unit weapon damage.", "+20% unit weapon damage."],
|
||||||
|
ImageFileName: "Improved Weaponry.png",
|
||||||
|
Body: "");
|
||||||
|
|
||||||
|
public static readonly PlanetaryMasteryItemData ValkaruArcheoalloys = new(
|
||||||
|
Id: "ValkaruArcheoalloys",
|
||||||
|
Name: "Valkaru Archeoalloys",
|
||||||
|
Levels: ["+1 armor to units.", "+1.5 armor to units.", "+2 armor to units.", "+2.5 armor to units.", "+3 armor to units."],
|
||||||
|
ImageFileName: "Valkaru Archeoalloys.png",
|
||||||
|
Body: "");
|
||||||
|
|
||||||
|
public static IReadOnlyList<PlanetaryMasteryItemData> All { get; } =
|
||||||
|
[
|
||||||
|
Accelerant,
|
||||||
|
Battery,
|
||||||
|
EnergySiphon,
|
||||||
|
FasterReload,
|
||||||
|
FluxCache,
|
||||||
|
GenesisPlating,
|
||||||
|
HexiteCache,
|
||||||
|
ImprovedWeaponry,
|
||||||
|
ValkaruArcheoalloys,
|
||||||
|
];
|
||||||
|
|
||||||
|
public static IReadOnlyDictionary<string, PlanetaryMasteryItemData> ByName { get; } =
|
||||||
|
All.ToDictionary(static item => item.Name, StringComparer.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
@@ -174,7 +174,7 @@ public static class StarSystems
|
|||||||
HigherChanceForItems: false,
|
HigherChanceForItems: false,
|
||||||
OccupationBuff: "+25 Starting Flux",
|
OccupationBuff: "+25 Starting Flux",
|
||||||
SystemMastery: "Max Supply",
|
SystemMastery: "Max Supply",
|
||||||
ConnectedSystems: ["Andus Reach", "Igneous Cascade", "Iotan Tears", "Idal's Word", "Sullen Dwarf"],
|
ConnectedSystems: ["Andus Reach", "Igneous Cascade", "Iotan Tears", "Idal's World", "Sullen Dwarf"],
|
||||||
X: 726.9,
|
X: 726.9,
|
||||||
Y: 403.5,
|
Y: 403.5,
|
||||||
Body: "");
|
Body: "");
|
||||||
@@ -451,9 +451,9 @@ public static class StarSystems
|
|||||||
Y: 280.8,
|
Y: 280.8,
|
||||||
Body: "");
|
Body: "");
|
||||||
|
|
||||||
public static readonly StarSystemData IdalSWord = new(
|
public static readonly StarSystemData IdalSWorld = new(
|
||||||
Id: "IdalSWord",
|
Id: "IdalSWorld",
|
||||||
Name: "Idal's Word",
|
Name: "Idal's World",
|
||||||
SystemCategory: StarSystemCategory.SideSystem,
|
SystemCategory: StarSystemCategory.SideSystem,
|
||||||
Owner: "The Eden Accord",
|
Owner: "The Eden Accord",
|
||||||
HigherInfluenceYield: false,
|
HigherInfluenceYield: false,
|
||||||
@@ -510,7 +510,7 @@ public static class StarSystems
|
|||||||
HigherChanceForItems: false,
|
HigherChanceForItems: false,
|
||||||
OccupationBuff: "+5 Topbar maximum energy",
|
OccupationBuff: "+5 Topbar maximum energy",
|
||||||
SystemMastery: "Production Speed for Buildings",
|
SystemMastery: "Production Speed for Buildings",
|
||||||
ConnectedSystems: ["Igneous Cascade", "Corundum Dream", "Idal's Word", "Heigen Stream"],
|
ConnectedSystems: ["Igneous Cascade", "Corundum Dream", "Idal's World", "Heigen Stream"],
|
||||||
X: 793.8,
|
X: 793.8,
|
||||||
Y: 476.4,
|
Y: 476.4,
|
||||||
Body: "");
|
Body: "");
|
||||||
@@ -1086,7 +1086,7 @@ public static class StarSystems
|
|||||||
HigherChanceForItems: false,
|
HigherChanceForItems: false,
|
||||||
OccupationBuff: "+3 Topbar starting energy",
|
OccupationBuff: "+3 Topbar starting energy",
|
||||||
SystemMastery: "Production Speed for Buildings",
|
SystemMastery: "Production Speed for Buildings",
|
||||||
ConnectedSystems: ["Quartzalian Berth", "Andus Reach", "Corundum Dream", "Idal's Word", "Mercury Forge", "Ivory Steel"],
|
ConnectedSystems: ["Quartzalian Berth", "Andus Reach", "Corundum Dream", "Idal's World", "Mercury Forge", "Ivory Steel"],
|
||||||
X: 658.7,
|
X: 658.7,
|
||||||
Y: 368.8,
|
Y: 368.8,
|
||||||
Body: "");
|
Body: "");
|
||||||
@@ -1345,7 +1345,7 @@ public static class StarSystems
|
|||||||
HeigenStream,
|
HeigenStream,
|
||||||
HeliosCrossing,
|
HeliosCrossing,
|
||||||
HyperionNebula,
|
HyperionNebula,
|
||||||
IdalSWord,
|
IdalSWorld,
|
||||||
IgneousCascade,
|
IgneousCascade,
|
||||||
InfernoSEdgeStronghold,
|
InfernoSEdgeStronghold,
|
||||||
IotanTears,
|
IotanTears,
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ public static class SystemMasteries
|
|||||||
Id: "HeroDamage",
|
Id: "HeroDamage",
|
||||||
Name: "Hero Damage",
|
Name: "Hero Damage",
|
||||||
Levels: ["+3% hero damage", "+3% hero damage", "+3% hero damage", "+3% hero damage", "-5% cooldown on heroes abilities"],
|
Levels: ["+3% hero damage", "+3% hero damage", "+3% hero damage", "+3% hero damage", "-5% cooldown on heroes abilities"],
|
||||||
AvailableSystems: ["Stronghold Atlas", "Hyperion Nebula", "Stronghold Leo", "Tartarus Expanse", "Shattered Light", "Lambda Gateway", "Devil's Trident"],
|
AvailableSystems: ["Stronghold Atlas", "Hyperion Nebula", "Stronghold Leo", "Tartarus Expanse", "Shattered Light", "Lambda Gateway", "Devil's Trident", "Beta Clarity"],
|
||||||
Stats: new Dictionary<string, double>
|
Stats: new Dictionary<string, double>
|
||||||
{
|
{
|
||||||
["heroDamage"] = 0.12,
|
["heroDamage"] = 0.12,
|
||||||
@@ -30,7 +30,7 @@ public static class SystemMasteries
|
|||||||
Id: "HeroHealthAndShields",
|
Id: "HeroHealthAndShields",
|
||||||
Name: "Hero Health and Shields",
|
Name: "Hero Health and Shields",
|
||||||
Levels: ["+3% hero health and shields", "+3% hero health and shields", "+3% hero health and shields", "+3% hero health and shields", "+4 to heroes health and shields regeneration rate"],
|
Levels: ["+3% hero health and shields", "+3% hero health and shields", "+3% hero health and shields", "+3% hero health and shields", "+4 to heroes health and shields regeneration rate"],
|
||||||
AvailableSystems: ["Yellow Dawn", "Gelid Storm", "Occam's Abode", "Scarlett Ire", "Negative Ion", "The Crucible", "Idal's Word", "Crimson Expanse"],
|
AvailableSystems: ["Yellow Dawn", "Gelid Storm", "Occam's Abode", "Scarlett Ire", "Negative Ion", "The Crucible", "Idal's World", "Crimson Expanse"],
|
||||||
Stats: new Dictionary<string, double>
|
Stats: new Dictionary<string, double>
|
||||||
{
|
{
|
||||||
["heroHealthAndShields"] = 0.12,
|
["heroHealthAndShields"] = 0.12,
|
||||||
@@ -42,7 +42,7 @@ public static class SystemMasteries
|
|||||||
Id: "MaxSupply",
|
Id: "MaxSupply",
|
||||||
Name: "Max Supply",
|
Name: "Max Supply",
|
||||||
Levels: ["+2 max supply", "+2 max supply", "+2 max supply", "+2 max supply", "+5 starting and max supply"],
|
Levels: ["+2 max supply", "+2 max supply", "+2 max supply", "+2 max supply", "+5 starting and max supply"],
|
||||||
AvailableSystems: ["Aurora Vale", "Corundum Dream", "Draconis Reach", "Stellar Forge", "Magnetic Folly", "Mercury Forge", "Wild's End"],
|
AvailableSystems: ["Aurora Vale", "Corundum Dream", "Draconis Reach", "Stellar Forge", "Magnetic Folly", "Mercury Forge", "Wild's End", "The Undergrowth"],
|
||||||
Stats: new Dictionary<string, double>
|
Stats: new Dictionary<string, double>
|
||||||
{
|
{
|
||||||
["startingSupply"] = 5,
|
["startingSupply"] = 5,
|
||||||
@@ -77,7 +77,7 @@ public static class SystemMasteries
|
|||||||
Id: "TurretDamage",
|
Id: "TurretDamage",
|
||||||
Name: "Turret Damage",
|
Name: "Turret Damage",
|
||||||
Levels: ["+5% turret damage.", "+5% turret damage.", "+5% turret damage.", "+5% turret damage.", "+10% turret attack speed."],
|
Levels: ["+5% turret damage.", "+5% turret damage.", "+5% turret damage.", "+5% turret damage.", "+10% turret attack speed."],
|
||||||
AvailableSystems: ["Solaris Reach", "Xenthic Fields", "Stronghold Vega", "Stronghold Tau", "Inferno's Edge Stronghold", "Titan's Fall", "Nebulon Drift"],
|
AvailableSystems: ["Solaris Reach", "Xenthic Fields", "Stronghold Vega", "Stronghold Tau", "Inferno's Edge Stronghold", "Titan's Fall", "Nebulon Drift", "Proxima Prime"],
|
||||||
Stats: new Dictionary<string, double>
|
Stats: new Dictionary<string, double>
|
||||||
{
|
{
|
||||||
["turretDamage"] = 0.2,
|
["turretDamage"] = 0.2,
|
||||||
@@ -92,6 +92,8 @@ public static class SystemMasteries
|
|||||||
AvailableSystems: ["Eden's Heart", "Crystallic Genesis", "Pleiades Cluster", "Ivory Fields", "Heigen Stream", "Gamma Swarm", "Helios Crossing", "Epsilon Arc", "Orion's Gate"],
|
AvailableSystems: ["Eden's Heart", "Crystallic Genesis", "Pleiades Cluster", "Ivory Fields", "Heigen Stream", "Gamma Swarm", "Helios Crossing", "Epsilon Arc", "Orion's Gate"],
|
||||||
Stats: new Dictionary<string, double>
|
Stats: new Dictionary<string, double>
|
||||||
{
|
{
|
||||||
|
["maxUnitEnergyAndRegen"] = 0.2,
|
||||||
|
["unitAbilityCooldown"] = -0.11,
|
||||||
},
|
},
|
||||||
Body: "I don't feel like there are good enough abilities that I would want to use this. I am sure this will rapidly change as more units get added to the roster. Perhaps that caster unit with [[Galavax]] is good.\nSome of the hotkeys on mercs are kind of bad, which is understandable given the sheer amount of variety. Perhaps I am also downplaying some of them, and tweaking the hotkeys will make them more friendly.\n\nI would ignore this for now, unless there is a very powerful caster unit added to the game. Like anything with summons via energy spending and cooldowns.");
|
Body: "I don't feel like there are good enough abilities that I would want to use this. I am sure this will rapidly change as more units get added to the roster. Perhaps that caster unit with [[Galavax]] is good.\nSome of the hotkeys on mercs are kind of bad, which is understandable given the sheer amount of variety. Perhaps I am also downplaying some of them, and tweaking the hotkeys will make them more friendly.\n\nI would ignore this for now, unless there is a very powerful caster unit added to the game. Like anything with summons via energy spending and cooldowns.");
|
||||||
|
|
||||||
|
|||||||
@@ -171,27 +171,6 @@ public static class Units
|
|||||||
BuildAtSameTime: 3,
|
BuildAtSameTime: 3,
|
||||||
Limit: null);
|
Limit: null);
|
||||||
|
|
||||||
public static readonly UnitData Harbinger = new(
|
|
||||||
Id: "Harbinger",
|
|
||||||
Name: "Harbinger",
|
|
||||||
Hexite: 75,
|
|
||||||
Flux: 50,
|
|
||||||
Supply: 4,
|
|
||||||
ProductionTime: 35,
|
|
||||||
Health: 400,
|
|
||||||
Energy: 0,
|
|
||||||
Shields: 0,
|
|
||||||
ArmorRating: 1,
|
|
||||||
MovementSpeed: 475,
|
|
||||||
DamagePerSecond: 14,
|
|
||||||
AttackRange: 6,
|
|
||||||
Attributes: ["Biological", "Medium Armor", "Bonus Damage vs Heavy Armor"],
|
|
||||||
Tier: 2,
|
|
||||||
Faction: "Grell",
|
|
||||||
Hotkey: "W",
|
|
||||||
BuildAtSameTime: 1,
|
|
||||||
Limit: null);
|
|
||||||
|
|
||||||
public static readonly UnitData Mandragora = new(
|
public static readonly UnitData Mandragora = new(
|
||||||
Id: "Mandragora",
|
Id: "Mandragora",
|
||||||
Name: "Mandragora",
|
Name: "Mandragora",
|
||||||
@@ -276,6 +255,300 @@ public static class Units
|
|||||||
BuildAtSameTime: 1,
|
BuildAtSameTime: 1,
|
||||||
Limit: null);
|
Limit: null);
|
||||||
|
|
||||||
|
public static readonly UnitData ScoutingDrone = new(
|
||||||
|
Id: "ScoutingDrone",
|
||||||
|
Name: "Scouting Drone",
|
||||||
|
Hexite: 25,
|
||||||
|
Flux: 0,
|
||||||
|
Supply: 0,
|
||||||
|
ProductionTime: 10,
|
||||||
|
Health: 75,
|
||||||
|
Energy: 0,
|
||||||
|
Shields: 0,
|
||||||
|
ArmorRating: 0,
|
||||||
|
MovementSpeed: 650,
|
||||||
|
DamagePerSecond: 0,
|
||||||
|
AttackRange: 0,
|
||||||
|
Attributes: ["Light Armor"],
|
||||||
|
Tier: 0,
|
||||||
|
Faction: "Xol",
|
||||||
|
Hotkey: null,
|
||||||
|
BuildAtSameTime: 1,
|
||||||
|
Limit: null);
|
||||||
|
|
||||||
|
public static readonly UnitData XolHarvester = new(
|
||||||
|
Id: "XolHarvester",
|
||||||
|
Name: "Xol Harvester",
|
||||||
|
Hexite: 175,
|
||||||
|
Flux: 0,
|
||||||
|
Supply: 0,
|
||||||
|
ProductionTime: 30,
|
||||||
|
Health: 400,
|
||||||
|
Energy: 0,
|
||||||
|
Shields: 0,
|
||||||
|
ArmorRating: 0,
|
||||||
|
MovementSpeed: 400,
|
||||||
|
DamagePerSecond: 0,
|
||||||
|
AttackRange: 0,
|
||||||
|
Attributes: ["Medium Armor", "Harvests Flux", "Respawns 60 seconds after death"],
|
||||||
|
Tier: 0,
|
||||||
|
Faction: "Xol",
|
||||||
|
Hotkey: null,
|
||||||
|
BuildAtSameTime: 1,
|
||||||
|
Limit: null);
|
||||||
|
|
||||||
|
public static readonly UnitData Roller = new(
|
||||||
|
Id: "Roller",
|
||||||
|
Name: "Roller",
|
||||||
|
Hexite: 25,
|
||||||
|
Flux: 0,
|
||||||
|
Supply: 1,
|
||||||
|
ProductionTime: 24,
|
||||||
|
Health: 80,
|
||||||
|
Energy: 0,
|
||||||
|
Shields: 0,
|
||||||
|
ArmorRating: 0,
|
||||||
|
MovementSpeed: 375,
|
||||||
|
DamagePerSecond: 10,
|
||||||
|
AttackRange: 2,
|
||||||
|
Attributes: ["Light Armor"],
|
||||||
|
Tier: 1,
|
||||||
|
Faction: "Xol",
|
||||||
|
Hotkey: null,
|
||||||
|
BuildAtSameTime: 1,
|
||||||
|
Limit: null);
|
||||||
|
|
||||||
|
public static readonly UnitData Spectron = new(
|
||||||
|
Id: "Spectron",
|
||||||
|
Name: "Spectron",
|
||||||
|
Hexite: 25,
|
||||||
|
Flux: 50,
|
||||||
|
Supply: 2,
|
||||||
|
ProductionTime: 5,
|
||||||
|
Health: 120,
|
||||||
|
Energy: 0,
|
||||||
|
Shields: 0,
|
||||||
|
ArmorRating: 0,
|
||||||
|
MovementSpeed: 600,
|
||||||
|
DamagePerSecond: 14,
|
||||||
|
AttackRange: 10,
|
||||||
|
Attributes: ["Light Armor", "Bonus Damage vs Heavy Armor"],
|
||||||
|
Tier: 1,
|
||||||
|
Faction: "Xol",
|
||||||
|
Hotkey: null,
|
||||||
|
BuildAtSameTime: 1,
|
||||||
|
Limit: null);
|
||||||
|
|
||||||
|
public static readonly UnitData Xyther = new(
|
||||||
|
Id: "Xyther",
|
||||||
|
Name: "Xyther",
|
||||||
|
Hexite: 75,
|
||||||
|
Flux: 0,
|
||||||
|
Supply: 2,
|
||||||
|
ProductionTime: 30,
|
||||||
|
Health: 150,
|
||||||
|
Energy: 0,
|
||||||
|
Shields: 100,
|
||||||
|
ArmorRating: 0,
|
||||||
|
MovementSpeed: 540,
|
||||||
|
DamagePerSecond: 12,
|
||||||
|
AttackRange: 1,
|
||||||
|
Attributes: ["Medium Armor", "Rebuilds after being destroyed"],
|
||||||
|
Tier: 1,
|
||||||
|
Faction: "Xol",
|
||||||
|
Hotkey: null,
|
||||||
|
BuildAtSameTime: 1,
|
||||||
|
Limit: null);
|
||||||
|
|
||||||
|
public static readonly UnitData Defiant = new(
|
||||||
|
Id: "Defiant",
|
||||||
|
Name: "Defiant",
|
||||||
|
Hexite: 150,
|
||||||
|
Flux: 50,
|
||||||
|
Supply: 4,
|
||||||
|
ProductionTime: 16,
|
||||||
|
Health: 500,
|
||||||
|
Energy: 6,
|
||||||
|
Shields: 0,
|
||||||
|
ArmorRating: 2,
|
||||||
|
MovementSpeed: 575,
|
||||||
|
DamagePerSecond: 29,
|
||||||
|
AttackRange: 2,
|
||||||
|
Attributes: ["Heavy Armor", "Bonus Damage vs Light Armor"],
|
||||||
|
Tier: 2,
|
||||||
|
Faction: "Xol",
|
||||||
|
Hotkey: null,
|
||||||
|
BuildAtSameTime: 1,
|
||||||
|
Limit: null);
|
||||||
|
|
||||||
|
public static readonly UnitData Echo = new(
|
||||||
|
Id: "Echo",
|
||||||
|
Name: "Echo",
|
||||||
|
Hexite: 50,
|
||||||
|
Flux: 50,
|
||||||
|
Supply: 2,
|
||||||
|
ProductionTime: 16,
|
||||||
|
Health: 125,
|
||||||
|
Energy: 0,
|
||||||
|
Shields: 0,
|
||||||
|
ArmorRating: 0,
|
||||||
|
MovementSpeed: 800,
|
||||||
|
DamagePerSecond: 7,
|
||||||
|
AttackRange: 5,
|
||||||
|
Attributes: ["Medium Armor", "Bonus Damage vs Medium Armor"],
|
||||||
|
Tier: 2,
|
||||||
|
Faction: "Xol",
|
||||||
|
Hotkey: null,
|
||||||
|
BuildAtSameTime: 1,
|
||||||
|
Limit: null);
|
||||||
|
|
||||||
|
public static readonly UnitData Havoc = new(
|
||||||
|
Id: "Havoc",
|
||||||
|
Name: "Havoc",
|
||||||
|
Hexite: 200,
|
||||||
|
Flux: 200,
|
||||||
|
Supply: 4,
|
||||||
|
ProductionTime: 0,
|
||||||
|
Health: 400,
|
||||||
|
Energy: 0,
|
||||||
|
Shields: 0,
|
||||||
|
ArmorRating: 1,
|
||||||
|
MovementSpeed: 900,
|
||||||
|
DamagePerSecond: 0,
|
||||||
|
AttackRange: 0,
|
||||||
|
Attributes: ["Medium Armor", "Transforms from Roller"],
|
||||||
|
Tier: 2,
|
||||||
|
Faction: "Xol",
|
||||||
|
Hotkey: null,
|
||||||
|
BuildAtSameTime: 1,
|
||||||
|
Limit: null);
|
||||||
|
|
||||||
|
public static readonly UnitData Synapse = new(
|
||||||
|
Id: "Synapse",
|
||||||
|
Name: "Synapse",
|
||||||
|
Hexite: 50,
|
||||||
|
Flux: 150,
|
||||||
|
Supply: 4,
|
||||||
|
ProductionTime: 16,
|
||||||
|
Health: 250,
|
||||||
|
Energy: 0,
|
||||||
|
Shields: 0,
|
||||||
|
ArmorRating: 0,
|
||||||
|
MovementSpeed: 675,
|
||||||
|
DamagePerSecond: 17,
|
||||||
|
AttackRange: 6,
|
||||||
|
Attributes: ["Light Armor"],
|
||||||
|
Tier: 2,
|
||||||
|
Faction: "Xol",
|
||||||
|
Hotkey: null,
|
||||||
|
BuildAtSameTime: 1,
|
||||||
|
Limit: null);
|
||||||
|
|
||||||
|
public static readonly UnitData Aegis = new(
|
||||||
|
Id: "Aegis",
|
||||||
|
Name: "Aegis",
|
||||||
|
Hexite: 275,
|
||||||
|
Flux: 100,
|
||||||
|
Supply: 6,
|
||||||
|
ProductionTime: 5,
|
||||||
|
Health: 600,
|
||||||
|
Energy: 0,
|
||||||
|
Shields: 100,
|
||||||
|
ArmorRating: 2,
|
||||||
|
MovementSpeed: 540,
|
||||||
|
DamagePerSecond: 11,
|
||||||
|
AttackRange: 6,
|
||||||
|
Attributes: ["Heavy Armor"],
|
||||||
|
Tier: 3,
|
||||||
|
Faction: "Xol",
|
||||||
|
Hotkey: null,
|
||||||
|
BuildAtSameTime: 1,
|
||||||
|
Limit: null);
|
||||||
|
|
||||||
|
public static readonly UnitData Cataclyst = new(
|
||||||
|
Id: "Cataclyst",
|
||||||
|
Name: "Cataclyst",
|
||||||
|
Hexite: 0,
|
||||||
|
Flux: 0,
|
||||||
|
Supply: 15,
|
||||||
|
ProductionTime: 50,
|
||||||
|
Health: 1000,
|
||||||
|
Energy: 0,
|
||||||
|
Shields: 0,
|
||||||
|
ArmorRating: 1,
|
||||||
|
MovementSpeed: 350,
|
||||||
|
DamagePerSecond: 60,
|
||||||
|
AttackRange: 12,
|
||||||
|
Attributes: ["Heavy Armor"],
|
||||||
|
Tier: 3,
|
||||||
|
Faction: "Xol",
|
||||||
|
Hotkey: null,
|
||||||
|
BuildAtSameTime: 1,
|
||||||
|
Limit: null);
|
||||||
|
|
||||||
|
public static readonly UnitData Eradicator = new(
|
||||||
|
Id: "Eradicator",
|
||||||
|
Name: "Eradicator",
|
||||||
|
Hexite: 125,
|
||||||
|
Flux: 250,
|
||||||
|
Supply: 6,
|
||||||
|
ProductionTime: 24,
|
||||||
|
Health: 1000,
|
||||||
|
Energy: 0,
|
||||||
|
Shields: 0,
|
||||||
|
ArmorRating: 1,
|
||||||
|
MovementSpeed: 400,
|
||||||
|
DamagePerSecond: 7,
|
||||||
|
AttackRange: 16,
|
||||||
|
Attributes: ["Heavy Armor", "Bonus Damage vs Heavy Armor"],
|
||||||
|
Tier: 3,
|
||||||
|
Faction: "Xol",
|
||||||
|
Hotkey: null,
|
||||||
|
BuildAtSameTime: 1,
|
||||||
|
Limit: null);
|
||||||
|
|
||||||
|
public static readonly UnitData Gravitron = new(
|
||||||
|
Id: "Gravitron",
|
||||||
|
Name: "Gravitron",
|
||||||
|
Hexite: 125,
|
||||||
|
Flux: 100,
|
||||||
|
Supply: 3,
|
||||||
|
ProductionTime: 24,
|
||||||
|
Health: 200,
|
||||||
|
Energy: 0,
|
||||||
|
Shields: 200,
|
||||||
|
ArmorRating: 0,
|
||||||
|
MovementSpeed: 400,
|
||||||
|
DamagePerSecond: 10,
|
||||||
|
AttackRange: 8,
|
||||||
|
Attributes: ["Medium Armor"],
|
||||||
|
Tier: 3,
|
||||||
|
Faction: "Xol",
|
||||||
|
Hotkey: null,
|
||||||
|
BuildAtSameTime: 1,
|
||||||
|
Limit: null);
|
||||||
|
|
||||||
|
public static readonly UnitData Paradox = new(
|
||||||
|
Id: "Paradox",
|
||||||
|
Name: "Paradox",
|
||||||
|
Hexite: 0,
|
||||||
|
Flux: 0,
|
||||||
|
Supply: 10,
|
||||||
|
ProductionTime: 50,
|
||||||
|
Health: 750,
|
||||||
|
Energy: 0,
|
||||||
|
Shields: 750,
|
||||||
|
ArmorRating: 0,
|
||||||
|
MovementSpeed: 450,
|
||||||
|
DamagePerSecond: 14,
|
||||||
|
AttackRange: 13,
|
||||||
|
Attributes: ["Heavy Armor"],
|
||||||
|
Tier: 4,
|
||||||
|
Faction: "Xol",
|
||||||
|
Hotkey: null,
|
||||||
|
BuildAtSameTime: 1,
|
||||||
|
Limit: null);
|
||||||
|
|
||||||
public static IReadOnlyList<UnitData> All { get; } =
|
public static IReadOnlyList<UnitData> All { get; } =
|
||||||
[
|
[
|
||||||
CorsairRaiders,
|
CorsairRaiders,
|
||||||
@@ -286,10 +559,23 @@ public static class Units
|
|||||||
Gatherer,
|
Gatherer,
|
||||||
BroodGuard,
|
BroodGuard,
|
||||||
Stinger,
|
Stinger,
|
||||||
Harbinger,
|
|
||||||
Mandragora,
|
Mandragora,
|
||||||
Behemoth,
|
Behemoth,
|
||||||
Reaver,
|
Reaver,
|
||||||
Weaver,
|
Weaver,
|
||||||
|
ScoutingDrone,
|
||||||
|
XolHarvester,
|
||||||
|
Roller,
|
||||||
|
Spectron,
|
||||||
|
Xyther,
|
||||||
|
Defiant,
|
||||||
|
Echo,
|
||||||
|
Havoc,
|
||||||
|
Synapse,
|
||||||
|
Aegis,
|
||||||
|
Cataclyst,
|
||||||
|
Eradicator,
|
||||||
|
Gravitron,
|
||||||
|
Paradox,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
@using Telerik.Blazor.Components
|
|
||||||
@inherits LayoutComponentBase
|
@inherits LayoutComponentBase
|
||||||
|
|
||||||
<div class="page">
|
<div class="page">
|
||||||
@@ -8,9 +7,7 @@
|
|||||||
|
|
||||||
<main>
|
<main>
|
||||||
<article class="content px-4">
|
<article class="content px-4">
|
||||||
<TelerikRootComponent>
|
|
||||||
@Body
|
@Body
|
||||||
</TelerikRootComponent>
|
|
||||||
</article>
|
</article>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -73,6 +73,14 @@
|
|||||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='%2310b981' viewBox='0 0 16 16'%3E%3Ccircle cx='8' cy='8' r='6' stroke='%2310b981' stroke-width='1.3' fill='none'/%3E%3Ccircle cx='8' cy='8' r='2' fill='%2310b981'/%3E%3Cpath stroke='%2310b981' stroke-width='1.3' d='M8 0v4M8 12v4M0 8h4M12 8h4'/%3E%3C/svg%3E");
|
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='%2310b981' viewBox='0 0 16 16'%3E%3Ccircle cx='8' cy='8' r='6' stroke='%2310b981' stroke-width='1.3' fill='none'/%3E%3Ccircle cx='8' cy='8' r='2' fill='%2310b981'/%3E%3Cpath stroke='%2310b981' stroke-width='1.3' d='M8 0v4M8 12v4M0 8h4M12 8h4'/%3E%3C/svg%3E");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.bi-items-nav-menu {
|
||||||
|
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;
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
@page "/galaxy"
|
@page "/galaxy"
|
||||||
|
@page "/"
|
||||||
@using System.Globalization
|
@using System.Globalization
|
||||||
@using System.Text.RegularExpressions
|
@using System.Text.RegularExpressions
|
||||||
@inject NavigationManager Navigation
|
@inject NavigationManager Navigation
|
||||||
|
@inject IJSRuntime Js
|
||||||
|
@inject IGalaxyMapExportSerializer ExportSerializer
|
||||||
|
|
||||||
<PageTitle>Galaxy Map</PageTitle>
|
<PageTitle>Galaxy Map</PageTitle>
|
||||||
|
|
||||||
@@ -9,7 +12,8 @@
|
|||||||
<div class="d-flex flex-wrap align-items-center justify-content-between mb-3 gap-3">
|
<div class="d-flex flex-wrap align-items-center justify-content-between mb-3 gap-3">
|
||||||
<div>
|
<div>
|
||||||
<h1 class="h2 mb-1 text-light fw-bold">Galaxy Map</h1>
|
<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>
|
||||||
<div class="d-flex flex-wrap align-items-center gap-3">
|
<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="map-legend d-flex flex-wrap align-items-center gap-3 px-3 py-2 rounded">
|
||||||
@@ -34,6 +38,19 @@
|
|||||||
<span class="mx-1">•</span>
|
<span class="mx-1">•</span>
|
||||||
<span><strong>@_connections.Count</strong> Hyperlanes</span>
|
<span><strong>@_connections.Count</strong> Hyperlanes</span>
|
||||||
</div>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -41,7 +58,8 @@
|
|||||||
<div class="filter-bar zs-glass-card p-3 mb-3 d-flex flex-wrap align-items-center gap-3">
|
<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">
|
<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>
|
<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="">All Types</option>
|
||||||
<option value="@StarSystemCategory.HomeworldSystem">Homeworld System</option>
|
<option value="@StarSystemCategory.HomeworldSystem">Homeworld System</option>
|
||||||
<option value="@StarSystemCategory.CoreSystem">Core System</option>
|
<option value="@StarSystemCategory.CoreSystem">Core System</option>
|
||||||
@@ -69,7 +87,7 @@
|
|||||||
<option value="Influence">Higher Influence</option>
|
<option value="Influence">Higher Influence</option>
|
||||||
<option value="Credits">Higher Credits</option>
|
<option value="Credits">Higher Credits</option>
|
||||||
<option value="Energy">Higher Energy</option>
|
<option value="Energy">Higher Energy</option>
|
||||||
<option value="Items">Higher Items</option>
|
<option value="Items">Higher Items (Currently, items are only attainable in shops)</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -184,13 +202,19 @@
|
|||||||
<circle r="@SvgNumber(radius)" class="node-circle"/>
|
<circle r="@SvgNumber(radius)" class="node-circle"/>
|
||||||
|
|
||||||
<!-- Center core dot for homeworlds and core systems -->
|
<!-- Center core dot for homeworlds and core systems -->
|
||||||
@if (system.SystemCategory != StarSystemCategory.SideSystem)
|
@if (system.SystemCategory == StarSystemCategory.CoreSystem)
|
||||||
{
|
{
|
||||||
<circle r="@SvgNumber(radius * 0.4)" class="node-core-dot"/>
|
<circle r="@SvgNumber(radius * 0.4)" class="node-core-dot"/>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@if (system.SystemCategory == StarSystemCategory.HomeworldSystem)
|
||||||
|
{
|
||||||
|
<circle r="@SvgNumber(radius * 0.15)" class="node-core-dot"/>
|
||||||
|
}
|
||||||
|
|
||||||
<!-- System Name Label -->
|
<!-- System Name Label -->
|
||||||
<text y="@SvgNumber(radius + 10)" class="node-label" text-anchor="middle">@system.Name</text>
|
<text y="@SvgNumber(radius + 10)" class="node-label"
|
||||||
|
text-anchor="middle">@system.Name</text>
|
||||||
</g>
|
</g>
|
||||||
}
|
}
|
||||||
</g>
|
</g>
|
||||||
@@ -210,7 +234,8 @@
|
|||||||
<span class="system-owner-indicator"></span>
|
<span class="system-owner-indicator"></span>
|
||||||
<span class="system-title fw-bold text-light">@_hoveredSystem.Name</span>
|
<span class="system-title fw-bold text-light">@_hoveredSystem.Name</span>
|
||||||
</div>
|
</div>
|
||||||
<span class="badge @CategoryBadgeClass(_hoveredSystem.SystemCategory)">@_hoveredSystem.SystemCategory.ToDisplayName()</span>
|
<span
|
||||||
|
class="badge @CategoryBadgeClass(_hoveredSystem.SystemCategory)">@_hoveredSystem.SystemCategory.ToDisplayName()</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card-details-grid">
|
<div class="card-details-grid">
|
||||||
@@ -221,14 +246,16 @@
|
|||||||
|
|
||||||
<div class="detail-row">
|
<div class="detail-row">
|
||||||
<span class="detail-label">Occupation Buff</span>
|
<span class="detail-label">Occupation Buff</span>
|
||||||
<span class="detail-value @(_hoveredSystem.OccupationBuff != null ? "text-buff" : "text-secondary")">
|
<span
|
||||||
|
class="detail-value @(_hoveredSystem.OccupationBuff != null ? "text-buff" : "text-secondary")">
|
||||||
@(_hoveredSystem.OccupationBuff ?? "—")
|
@(_hoveredSystem.OccupationBuff ?? "—")
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="detail-row">
|
<div class="detail-row">
|
||||||
<span class="detail-label">System Mastery</span>
|
<span class="detail-label">System Mastery</span>
|
||||||
<span class="detail-value @(_hoveredSystem.SystemMastery != null ? "text-mastery" : "text-secondary")">
|
<span
|
||||||
|
class="detail-value @(_hoveredSystem.SystemMastery != null ? "text-mastery" : "text-secondary")">
|
||||||
@(_hoveredSystem.SystemMastery ?? "—")
|
@(_hoveredSystem.SystemMastery ?? "—")
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -253,7 +280,8 @@
|
|||||||
<div class="detail-row">
|
<div class="detail-row">
|
||||||
<div class="d-flex align-items-center justify-content-between">
|
<div class="d-flex align-items-center justify-content-between">
|
||||||
<span class="detail-label">Connected Systems</span>
|
<span class="detail-label">Connected Systems</span>
|
||||||
<span class="badge bg-dark text-secondary border border-secondary border-opacity-25">@_hoveredSystem.ConnectedSystems.Count</span>
|
<span
|
||||||
|
class="badge bg-dark text-secondary border border-secondary border-opacity-25">@_hoveredSystem.ConnectedSystems.Count</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="connected-systems-list text-secondary small mt-1">
|
<div class="connected-systems-list text-secondary small mt-1">
|
||||||
@string.Join(" • ", _hoveredSystem.ConnectedSystems)
|
@string.Join(" • ", _hoveredSystem.ConnectedSystems)
|
||||||
@@ -268,7 +296,8 @@
|
|||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<aside class="faction-drawer zs-glass-card @(_drawerCollapsed ? "is-collapsed" : "")" aria-label="Faction occupational bonuses">
|
<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 class="drawer-header d-flex align-items-center justify-content-between gap-2">
|
||||||
<div>
|
<div>
|
||||||
<h2 class="drawer-title text-light fw-bold mb-0">Faction Bonuses</h2>
|
<h2 class="drawer-title text-light fw-bold mb-0">Faction Bonuses</h2>
|
||||||
@@ -306,7 +335,8 @@
|
|||||||
</p>
|
</p>
|
||||||
@if (FactionBonuses.Count == 0)
|
@if (FactionBonuses.Count == 0)
|
||||||
{
|
{
|
||||||
<p class="text-secondary small mb-0">No occupation buffs recorded for this faction's systems.</p>
|
<p class="text-secondary small mb-0">No occupation buffs recorded for this faction's
|
||||||
|
systems.</p>
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -316,7 +346,8 @@
|
|||||||
<li class="bonus-row">
|
<li class="bonus-row">
|
||||||
<span class="bonus-total">@FormatBonusTotal(bonus)</span>
|
<span class="bonus-total">@FormatBonusTotal(bonus)</span>
|
||||||
<span class="bonus-label">@bonus.Label</span>
|
<span class="bonus-label">@bonus.Label</span>
|
||||||
<span class="bonus-count">@bonus.PlanetCount @(bonus.PlanetCount == 1 ? "planet" : "planets")</span>
|
<span
|
||||||
|
class="bonus-count">@bonus.PlanetCount @(bonus.PlanetCount == 1 ? "planet" : "planets")</span>
|
||||||
</li>
|
</li>
|
||||||
}
|
}
|
||||||
</ul>
|
</ul>
|
||||||
@@ -329,7 +360,8 @@
|
|||||||
@foreach (var system in FactionSystems)
|
@foreach (var system in FactionSystems)
|
||||||
{
|
{
|
||||||
<li>
|
<li>
|
||||||
<button class="planet-row" @onclick="() => OnNodeClick(system.Id)" title="View @system.Name details">
|
<button class="planet-row" @onclick="() => OnNodeClick(system.Id)"
|
||||||
|
title="View @system.Name details">
|
||||||
<span class="planet-name">@system.Name</span>
|
<span class="planet-name">@system.Name</span>
|
||||||
<span class="planet-buff">@(system.OccupationBuff ?? "—")</span>
|
<span class="planet-buff">@(system.OccupationBuff ?? "—")</span>
|
||||||
</button>
|
</button>
|
||||||
@@ -362,6 +394,7 @@
|
|||||||
// Faction picker: one of the three factions is always selected.
|
// Faction picker: one of the three factions is always selected.
|
||||||
private string _selectedFaction = StarSystemOwners.EdenAccord;
|
private string _selectedFaction = StarSystemOwners.EdenAccord;
|
||||||
private bool _drawerCollapsed;
|
private bool _drawerCollapsed;
|
||||||
|
private bool _isExporting;
|
||||||
|
|
||||||
private bool HasActiveFilters =>
|
private bool HasActiveFilters =>
|
||||||
_selectedCategory.HasValue ||
|
_selectedCategory.HasValue ||
|
||||||
@@ -386,6 +419,7 @@
|
|||||||
_matchingSystemIds.Add(system.Id);
|
_matchingSystemIds.Add(system.Id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_matchingCount = _matchingSystemIds.Count;
|
_matchingCount = _matchingSystemIds.Count;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -434,6 +468,23 @@
|
|||||||
ApplyFilters();
|
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 -----
|
// ----- faction bonus drawer -----
|
||||||
|
|
||||||
public sealed record FactionBonus(string Label, bool IsPercent, double Total, int PlanetCount);
|
public sealed record FactionBonus(string Label, bool IsPercent, double Total, int PlanetCount);
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
@page "/"
|
|
||||||
@using ZeroSpace.Model
|
@using ZeroSpace.Model
|
||||||
|
|
||||||
<PageTitle>ZeroSpace Database & Command Hub</PageTitle>
|
<PageTitle>ZeroSpace Database & Command Hub</PageTitle>
|
||||||
|
|||||||
@@ -0,0 +1,405 @@
|
|||||||
|
@page "/loadout"
|
||||||
|
|
||||||
|
<PageTitle>Loadout</PageTitle>
|
||||||
|
|
||||||
|
<div class="section-header d-flex align-items-center mb-4 flex-wrap gap-2">
|
||||||
|
<h1 class="mb-0">Loadout</h1>
|
||||||
|
<div class="ms-4 d-flex align-items-center gap-2">
|
||||||
|
<span class="hero-label">Hero:</span>
|
||||||
|
@foreach (var hero in Heroes)
|
||||||
|
{
|
||||||
|
<button type="button"
|
||||||
|
class="btn @(hero == _hero ? "btn-primary" : "btn-outline-secondary") btn-sm"
|
||||||
|
@onclick="() => SelectHero(hero)">
|
||||||
|
@hero
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
</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 class="loadout-layout">
|
||||||
|
<section class="loadout-pool" aria-label="Available items">
|
||||||
|
<label class="details-toggle">
|
||||||
|
<input type="checkbox" @bind="_showDetails" />
|
||||||
|
Show details
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<h2 class="pool-heading">Faction Powers</h2>
|
||||||
|
<div class="pool-list @(_showDetails ? "" : "compact")">
|
||||||
|
@foreach (var item in FactionPowers)
|
||||||
|
{
|
||||||
|
<article class="zs-glass-card pool-card @(_showDetails ? "" : "icon-only") @(_factionSlots.Contains(item) ? "equipped" : "")"
|
||||||
|
draggable="true"
|
||||||
|
title="@item.Name"
|
||||||
|
@ondragstart="() => _drag = item"
|
||||||
|
@ondragend="() => _drag = null">
|
||||||
|
@if (item.ImageUrl is not null)
|
||||||
|
{
|
||||||
|
<img class="pool-icon" src="@item.ImageUrl" alt="@item.Name" loading="lazy" draggable="false" />
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<div class="pool-icon pool-icon-placeholder" aria-hidden="true">✦</div>
|
||||||
|
}
|
||||||
|
@if (_showDetails)
|
||||||
|
{
|
||||||
|
<div class="pool-body">
|
||||||
|
<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)
|
||||||
|
{
|
||||||
|
<span class="merc-badge">@item.LimitedToMerc</span>
|
||||||
|
}
|
||||||
|
<ul class="pool-levels">
|
||||||
|
@foreach (var level in item.Levels)
|
||||||
|
{
|
||||||
|
<li>@level</li>
|
||||||
|
}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</article>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2 class="pool-heading">General Items</h2>
|
||||||
|
<div class="pool-list @(_showDetails ? "" : "compact")">
|
||||||
|
@foreach (var item in GeneralItemsList)
|
||||||
|
{
|
||||||
|
<article class="zs-glass-card pool-card @(_showDetails ? "" : "icon-only") @(_generalSlots.Contains(item) ? "equipped" : "")"
|
||||||
|
draggable="true"
|
||||||
|
title="@item.Name"
|
||||||
|
@ondragstart="() => _drag = item"
|
||||||
|
@ondragend="() => _drag = null">
|
||||||
|
@if (item.ImageUrl is not null)
|
||||||
|
{
|
||||||
|
<img class="pool-icon" src="@item.ImageUrl" alt="@item.Name" loading="lazy" draggable="false" />
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<div class="pool-icon pool-icon-placeholder" aria-hidden="true">✦</div>
|
||||||
|
}
|
||||||
|
@if (_showDetails)
|
||||||
|
{
|
||||||
|
<div class="pool-body">
|
||||||
|
<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)
|
||||||
|
{
|
||||||
|
<span class="merc-badge">@item.LimitedToMerc</span>
|
||||||
|
}
|
||||||
|
<ul class="pool-levels">
|
||||||
|
@foreach (var level in item.Levels)
|
||||||
|
{
|
||||||
|
<li>@level</li>
|
||||||
|
}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</article>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2 class="pool-heading">Master Items</h2>
|
||||||
|
<div class="pool-list @(_showDetails ? "" : "compact")">
|
||||||
|
@foreach (var item in MasterItems)
|
||||||
|
{
|
||||||
|
<article class="zs-glass-card pool-card @(_showDetails ? "" : "icon-only") @(_masterySlots.Contains(item) ? "equipped" : "")"
|
||||||
|
draggable="true"
|
||||||
|
title="@item.Name"
|
||||||
|
@ondragstart="() => _drag = item"
|
||||||
|
@ondragend="() => _drag = null">
|
||||||
|
@if (item.ImageUrl is not null)
|
||||||
|
{
|
||||||
|
<img class="pool-icon" src="@item.ImageUrl" alt="@item.Name" loading="lazy" draggable="false" />
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<div class="pool-icon pool-icon-placeholder" aria-hidden="true">✦</div>
|
||||||
|
}
|
||||||
|
@if (_showDetails)
|
||||||
|
{
|
||||||
|
<div class="pool-body">
|
||||||
|
<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">
|
||||||
|
@foreach (var level in item.Levels)
|
||||||
|
{
|
||||||
|
<li>@level</li>
|
||||||
|
}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</article>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="loadout-board" aria-label="Current loadout">
|
||||||
|
@if (_mercError is not null)
|
||||||
|
{
|
||||||
|
<div class="merc-error" role="alert">@_mercError</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
<h2 class="pool-heading">Faction Powers <span class="slot-count">pick up to 4</span></h2>
|
||||||
|
<div class="slot-grid">
|
||||||
|
@for (var i = 0; i < _factionSlots.Length; i++)
|
||||||
|
{
|
||||||
|
var index = i;
|
||||||
|
<div class="slot @(IsConflict(_factionSlots[index]) ? "slot-conflict" : "") @(_factionSlots[index] is null ? "slot-empty" : "")"
|
||||||
|
@ondragover:preventDefault
|
||||||
|
@ondrop="() => DropFaction(index)">
|
||||||
|
@if (_factionSlots[index] is { } slotted)
|
||||||
|
{
|
||||||
|
<button type="button" class="slot-remove" @onclick="() => RemoveFaction(index)" aria-label="Remove @slotted.Name">×</button>
|
||||||
|
@if (slotted.ImageUrl is not null)
|
||||||
|
{
|
||||||
|
<img class="slot-icon" src="@slotted.ImageUrl" alt="" draggable="false" />
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<div class="slot-icon slot-icon-placeholder" aria-hidden="true">✦</div>
|
||||||
|
}
|
||||||
|
<span class="slot-name">@slotted.Name</span>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<span class="slot-hint">Drop here</span>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2 class="pool-heading">General Items <span class="slot-count">pick up to 3</span></h2>
|
||||||
|
<div class="slot-grid">
|
||||||
|
@for (var i = 0; i < _generalSlots.Length; i++)
|
||||||
|
{
|
||||||
|
var index = i;
|
||||||
|
<div class="slot @(IsConflict(_generalSlots[index]) ? "slot-conflict" : "") @(_generalSlots[index] is null ? "slot-empty" : "")"
|
||||||
|
@ondragover:preventDefault
|
||||||
|
@ondrop="() => DropGeneral(index)">
|
||||||
|
@if (_generalSlots[index] is { } slotted)
|
||||||
|
{
|
||||||
|
<button type="button" class="slot-remove" @onclick="() => RemoveGeneral(index)" aria-label="Remove @slotted.Name">×</button>
|
||||||
|
@if (slotted.ImageUrl is not null)
|
||||||
|
{
|
||||||
|
<img class="slot-icon" src="@slotted.ImageUrl" alt="" draggable="false" />
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<div class="slot-icon slot-icon-placeholder" aria-hidden="true">✦</div>
|
||||||
|
}
|
||||||
|
<span class="slot-name">@slotted.Name</span>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<span class="slot-hint">Drop here</span>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2 class="pool-heading">Master Items <span class="slot-count">pick up to 2</span></h2>
|
||||||
|
<div class="slot-grid">
|
||||||
|
@for (var i = 0; i < _masterySlots.Length; i++)
|
||||||
|
{
|
||||||
|
var index = i;
|
||||||
|
<div class="slot @(IsConflict(_masterySlots[index]) ? "slot-conflict" : "") @(_masterySlots[index] is null ? "slot-empty" : "")"
|
||||||
|
@ondragover:preventDefault
|
||||||
|
@ondrop="() => DropMastery(index)">
|
||||||
|
@if (_masterySlots[index] is { } slotted)
|
||||||
|
{
|
||||||
|
<button type="button" class="slot-remove" @onclick="() => RemoveMastery(index)" aria-label="Remove @slotted.Name">×</button>
|
||||||
|
@if (slotted.ImageUrl is not null)
|
||||||
|
{
|
||||||
|
<img class="slot-icon" src="@slotted.ImageUrl" alt="" draggable="false" />
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<div class="slot-icon slot-icon-placeholder" aria-hidden="true">✦</div>
|
||||||
|
}
|
||||||
|
<span class="slot-name">@slotted.Name</span>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<span class="slot-hint">Drop here</span>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@code {
|
||||||
|
private sealed record PoolItem(
|
||||||
|
string Kind,
|
||||||
|
string Id,
|
||||||
|
string Name,
|
||||||
|
IReadOnlyList<string> Levels,
|
||||||
|
string? LimitedTo,
|
||||||
|
string? LimitedToMerc,
|
||||||
|
string? ImageUrl,
|
||||||
|
string DocUrl);
|
||||||
|
|
||||||
|
private static readonly string[] Heroes = ["Vynthra", "Prefect Aster"];
|
||||||
|
|
||||||
|
private string _hero = Heroes[0];
|
||||||
|
private bool _showDetails = true;
|
||||||
|
private PoolItem? _drag;
|
||||||
|
|
||||||
|
private List<PoolItem> _allFactionPowers = [];
|
||||||
|
private List<PoolItem> _allGeneralItems = [];
|
||||||
|
private List<PoolItem> _allMasterItems = [];
|
||||||
|
|
||||||
|
private PoolItem?[] _factionSlots = new PoolItem?[4];
|
||||||
|
private PoolItem?[] _generalSlots = new PoolItem?[3];
|
||||||
|
private PoolItem?[] _masterySlots = new PoolItem?[2];
|
||||||
|
|
||||||
|
private readonly HashSet<PoolItem> _mercConflicts = [];
|
||||||
|
private string? _mercError;
|
||||||
|
|
||||||
|
private IEnumerable<PoolItem> FactionPowers =>
|
||||||
|
_allFactionPowers.Where(item => item.LimitedTo is null || item.LimitedTo == _hero);
|
||||||
|
|
||||||
|
private IEnumerable<PoolItem> GeneralItemsList => _allGeneralItems;
|
||||||
|
|
||||||
|
private IEnumerable<PoolItem> MasterItems => _allMasterItems;
|
||||||
|
|
||||||
|
protected override void OnInitialized()
|
||||||
|
{
|
||||||
|
_allFactionPowers = FactionPowerItems.All
|
||||||
|
.Select(item => new PoolItem(
|
||||||
|
"faction",
|
||||||
|
item.Id,
|
||||||
|
item.Name,
|
||||||
|
item.Levels,
|
||||||
|
item.LimitedTo,
|
||||||
|
item.LimitedToMerc,
|
||||||
|
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();
|
||||||
|
|
||||||
|
_allGeneralItems = GeneralItems.All
|
||||||
|
.Select(item => new PoolItem(
|
||||||
|
"general",
|
||||||
|
item.Id,
|
||||||
|
item.Name,
|
||||||
|
item.Levels,
|
||||||
|
LimitedTo: null,
|
||||||
|
item.LimitedToMerc,
|
||||||
|
item.ImageFileName is null ? null : $"images/general-items/{Uri.EscapeDataString(item.ImageFileName)}",
|
||||||
|
ReferenceCatalog.GetUrlFor(item.Name) ?? $"/reference/general-item/{ReferenceCatalog.ToSlug(item.Name)}"))
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
_allMasterItems = PlanetaryMasteryItems.All
|
||||||
|
.Select(item => new PoolItem(
|
||||||
|
"mastery",
|
||||||
|
item.Id,
|
||||||
|
item.Name,
|
||||||
|
item.Levels,
|
||||||
|
LimitedTo: null,
|
||||||
|
LimitedToMerc: null,
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SelectHero(string hero)
|
||||||
|
{
|
||||||
|
if (_hero == hero)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_hero = hero;
|
||||||
|
|
||||||
|
// Faction powers are hero-locked: unequip any that belong to another hero.
|
||||||
|
for (var i = 0; i < _factionSlots.Length; i++)
|
||||||
|
{
|
||||||
|
var slotted = _factionSlots[i];
|
||||||
|
if (slotted?.LimitedTo is not null && slotted.LimitedTo != _hero)
|
||||||
|
{
|
||||||
|
_factionSlots[i] = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ValidateMercs();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DropFaction(int index) => DropInto(_factionSlots, "faction", index);
|
||||||
|
private void DropGeneral(int index) => DropInto(_generalSlots, "general", index);
|
||||||
|
private void DropMastery(int index) => DropInto(_masterySlots, "mastery", index);
|
||||||
|
|
||||||
|
private void DropInto(PoolItem?[] slots, string kind, int index)
|
||||||
|
{
|
||||||
|
var dragged = _drag;
|
||||||
|
_drag = null;
|
||||||
|
if (dragged is null || dragged.Kind != kind)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (slots.Contains(dragged) || slots[index] is not null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
slots[index] = dragged;
|
||||||
|
ValidateMercs();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RemoveFaction(int index) => RemoveFrom(_factionSlots, index);
|
||||||
|
private void RemoveGeneral(int index) => RemoveFrom(_generalSlots, index);
|
||||||
|
private void RemoveMastery(int index) => RemoveFrom(_masterySlots, index);
|
||||||
|
|
||||||
|
private void RemoveFrom(PoolItem?[] slots, int index)
|
||||||
|
{
|
||||||
|
slots[index] = null;
|
||||||
|
ValidateMercs();
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool IsConflict(PoolItem? item) => item is not null && _mercConflicts.Contains(item);
|
||||||
|
|
||||||
|
private IEnumerable<PoolItem> SlottedItems() =>
|
||||||
|
_factionSlots.Concat(_generalSlots).Concat(_masterySlots).Where(static item => item is not null)!;
|
||||||
|
|
||||||
|
private void ValidateMercs()
|
||||||
|
{
|
||||||
|
var mercs = SlottedItems()
|
||||||
|
.Where(static item => item.LimitedToMerc is not null)
|
||||||
|
.Select(static item => item.LimitedToMerc!)
|
||||||
|
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
_mercConflicts.Clear();
|
||||||
|
if (mercs.Count > 1)
|
||||||
|
{
|
||||||
|
foreach (var item in SlottedItems().Where(static item => item.LimitedToMerc is not null))
|
||||||
|
{
|
||||||
|
_mercConflicts.Add(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
_mercError = "Two items cannot be from different mercs.";
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_mercError = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,258 @@
|
|||||||
|
.loadout-layout {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 1.5rem;
|
||||||
|
align-items: start;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 900px) {
|
||||||
|
.loadout-layout {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* The item pool scrolls independently so the loadout board stays on screen. */
|
||||||
|
.loadout-pool {
|
||||||
|
max-height: calc(100vh - 10rem);
|
||||||
|
max-height: calc(100dvh - 10rem);
|
||||||
|
overflow-y: auto;
|
||||||
|
padding-right: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 900px) {
|
||||||
|
.loadout-pool {
|
||||||
|
max-height: none;
|
||||||
|
overflow-y: visible;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-label {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #94a3b8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.details-toggle {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
cursor: pointer;
|
||||||
|
color: #cbd5e1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pool-heading {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #fff;
|
||||||
|
margin: 1.5rem 0 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pool-heading:first-of-type {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slot-count {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
font-weight: 400;
|
||||||
|
color: #94a3b8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pool-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Icon-only mode: icons wrap in a compact flex grid and shrink a little. */
|
||||||
|
.pool-list.compact {
|
||||||
|
flex-direction: row;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pool-list.compact .pool-card {
|
||||||
|
padding: 0.3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pool-list.compact .pool-icon {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pool-card {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.75rem;
|
||||||
|
align-items: flex-start;
|
||||||
|
padding: 0.6rem 0.75rem;
|
||||||
|
cursor: grab;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pool-card:active {
|
||||||
|
cursor: grabbing;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pool-card.icon-only {
|
||||||
|
padding: 0.35rem;
|
||||||
|
width: fit-content;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pool-card.equipped {
|
||||||
|
opacity: 0.45;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pool-icon {
|
||||||
|
width: 48px;
|
||||||
|
height: 48px;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid var(--zs-border-subtle);
|
||||||
|
background: rgba(0, 0, 0, 0.45);
|
||||||
|
flex-shrink: 0;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pool-icon-placeholder {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: #38bdf8;
|
||||||
|
font-size: 1.4rem;
|
||||||
|
text-shadow: 0 0 8px rgba(56, 189, 248, 0.8);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pool-body {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pool-name {
|
||||||
|
font-weight: 700;
|
||||||
|
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 {
|
||||||
|
display: inline-block;
|
||||||
|
font-size: 0.72rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #fbbf24;
|
||||||
|
border: 1px solid rgba(251, 191, 36, 0.5);
|
||||||
|
border-radius: 999px;
|
||||||
|
padding: 0.05rem 0.5rem;
|
||||||
|
margin: 0.25rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pool-levels {
|
||||||
|
list-style: none;
|
||||||
|
margin: 0.25rem 0 0;
|
||||||
|
padding: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.15rem;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: #cbd5e1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.merc-error {
|
||||||
|
background: rgba(220, 38, 38, 0.15);
|
||||||
|
border: 1px solid #dc2626;
|
||||||
|
color: #fca5a5;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 0.6rem 0.9rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slot-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
|
||||||
|
gap: 0.6rem;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slot {
|
||||||
|
position: relative;
|
||||||
|
min-height: 92px;
|
||||||
|
border-radius: 10px;
|
||||||
|
border: 1px solid var(--zs-border-subtle);
|
||||||
|
background: rgba(0, 0, 0, 0.35);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 0.3rem;
|
||||||
|
padding: 0.6rem 0.5rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slot-empty {
|
||||||
|
border-style: dashed;
|
||||||
|
border-color: rgba(148, 163, 184, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.slot-hint {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: #64748b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slot-icon {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
border-radius: 8px;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slot-icon-placeholder {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: #38bdf8;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
background: rgba(0, 0, 0, 0.45);
|
||||||
|
border: 1px solid var(--zs-border-subtle);
|
||||||
|
}
|
||||||
|
|
||||||
|
.slot-name {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #e2e8f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slot-remove {
|
||||||
|
position: absolute;
|
||||||
|
top: 2px;
|
||||||
|
right: 6px;
|
||||||
|
border: none;
|
||||||
|
background: none;
|
||||||
|
color: #94a3b8;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
line-height: 1;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 0 0.15rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slot-remove:hover {
|
||||||
|
color: #f87171;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slot-conflict {
|
||||||
|
border: 2px solid #dc2626;
|
||||||
|
box-shadow: 0 0 10px rgba(220, 38, 38, 0.6);
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
@page "/planetary-mastery-items"
|
||||||
|
|
||||||
|
<PageTitle>Planetary Mastery Items</PageTitle>
|
||||||
|
|
||||||
|
<div class="section-header d-flex align-items-center mb-4">
|
||||||
|
<h1 class="mb-0">Planetary Mastery Items</h1>
|
||||||
|
<span class="badge badge-mastery ms-3">@PlanetaryMasteryItems.All.Count Items</span>
|
||||||
|
<div class="ms-3 flex-grow-1 border-bottom opacity-25"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="pmi-grid">
|
||||||
|
@foreach (var item in PlanetaryMasteryItems.All.OrderBy(i => i.Name))
|
||||||
|
{
|
||||||
|
<article class="zs-glass-card pmi-card">
|
||||||
|
@if (item.ImageFileName is not null)
|
||||||
|
{
|
||||||
|
<img class="pmi-icon" src="images/planetary-mastery-items/@Uri.EscapeDataString(item.ImageFileName)" alt="@item.Name" loading="lazy" />
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<div class="pmi-icon pmi-icon-placeholder" aria-hidden="true">✦</div>
|
||||||
|
}
|
||||||
|
<div class="pmi-body">
|
||||||
|
<h2 class="pmi-name">@item.Name</h2>
|
||||||
|
<ol class="pmi-levels">
|
||||||
|
@for (var tier = 0; tier < item.Levels.Count; tier++)
|
||||||
|
{
|
||||||
|
var tierLabel = (tier + 1).ToString("D2");
|
||||||
|
<li class="pmi-level"><span class="pmi-tier">@tierLabel</span><span>@item.Levels[tier]</span></li>
|
||||||
|
}
|
||||||
|
</ol>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
.pmi-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(340px, 1fr));
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pmi-card {
|
||||||
|
display: flex;
|
||||||
|
gap: 1rem;
|
||||||
|
align-items: flex-start;
|
||||||
|
padding: 1rem 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pmi-icon {
|
||||||
|
width: 76px;
|
||||||
|
height: 76px;
|
||||||
|
border-radius: 10px;
|
||||||
|
border: 1px solid var(--zs-border-subtle);
|
||||||
|
background: rgba(0, 0, 0, 0.45);
|
||||||
|
flex-shrink: 0;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pmi-icon-placeholder {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: #38bdf8;
|
||||||
|
font-size: 2rem;
|
||||||
|
text-shadow: 0 0 10px rgba(56, 189, 248, 0.8);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pmi-body {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pmi-name {
|
||||||
|
font-size: 1.15rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #fff;
|
||||||
|
margin: 0 0 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pmi-levels {
|
||||||
|
list-style: none;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pmi-level {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.6rem;
|
||||||
|
align-items: baseline;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
color: #cbd5e1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pmi-tier {
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
color: #38bdf8;
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
min-width: 1.75rem;
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -59,7 +59,7 @@ else
|
|||||||
}
|
}
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
@if (!string.IsNullOrWhiteSpace(mastery.Body))
|
@if (!string.IsNullOrWhiteSpace(mastery.Body) && false) // Let's not display notes. They are very placeholder and probably wrong
|
||||||
{
|
{
|
||||||
<h2>Notes</h2>
|
<h2>Notes</h2>
|
||||||
<MarkdownView Markdown="@mastery.Body" />
|
<MarkdownView Markdown="@mastery.Body" />
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
@page "/units"
|
@page "/units"
|
||||||
@using ZeroSpace.Model
|
@using ZeroSpace.Model
|
||||||
@using ZeroSpace.Web.Services
|
@using ZeroSpace.Web.Services
|
||||||
@using Telerik.Blazor
|
|
||||||
@using Telerik.Blazor.Components
|
|
||||||
@inject IUnitsApiClient UnitsApiClient
|
@inject IUnitsApiClient UnitsApiClient
|
||||||
|
|
||||||
<PageTitle>Units</PageTitle>
|
<PageTitle>Units</PageTitle>
|
||||||
@@ -29,7 +27,7 @@ else if (errorMessage != null)
|
|||||||
<button class="btn btn-outline-danger btn-sm" @onclick="LoadUnitsAsync">Retry</button>
|
<button class="btn btn-outline-danger btn-sm" @onclick="LoadUnitsAsync">Retry</button>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
else if (gearNotes == null || gearNotes.Count == 0)
|
else if (units == null || units.Count == 0)
|
||||||
{
|
{
|
||||||
<div class="alert alert-warning" role="alert">
|
<div class="alert alert-warning" role="alert">
|
||||||
No units found.
|
No units found.
|
||||||
@@ -37,42 +35,159 @@ else if (gearNotes == null || gearNotes.Count == 0)
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
<div class="grid-container">
|
<div class="table-responsive">
|
||||||
<TelerikGrid Data="@gearNotes" Pageable="true" PageSize="50" Sortable="true" FilterMode="@GridFilterMode.FilterRow"
|
<table class="table table-striped table-hover table-sm align-middle text-nowrap">
|
||||||
Height="calc(100vh - 250px)">
|
<thead class="table-light">
|
||||||
<GridColumns>
|
<tr>
|
||||||
<GridColumn Field="@(nameof(UnitData.Name))" Title="Name" Width="200px"/>
|
@foreach (var col in Columns)
|
||||||
<GridColumn Field="@(nameof(UnitData.Hexite))" Title="Hexite" Width="90px" />
|
{
|
||||||
<GridColumn Field="@(nameof(UnitData.Flux))" Title="Flux" Width="90px" />
|
<th style="min-width:@col.Width">
|
||||||
|
<button type="button" class="btn btn-link btn-sm p-0 text-decoration-none text-body fw-bold" @onclick="() => ToggleSort(col.Key)" title="Sort by @col.Title">
|
||||||
|
@col.Title
|
||||||
|
<span class="text-muted">@SortGlyph(col.Key)</span>
|
||||||
|
</button>
|
||||||
|
</th>
|
||||||
|
}
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
@foreach (var col in Columns)
|
||||||
|
{
|
||||||
|
<th>
|
||||||
|
<input class="form-control form-control-sm" placeholder="Filter" aria-label="Filter @col.Title"
|
||||||
|
value="@GetFilter(col.Key)"
|
||||||
|
@oninput="(e) => SetFilter(col.Key, e.Value?.ToString())" />
|
||||||
|
</th>
|
||||||
|
}
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
@foreach (var unit in PagedUnits)
|
||||||
|
{
|
||||||
|
<tr>
|
||||||
|
@foreach (var col in Columns)
|
||||||
|
{
|
||||||
|
<td>@col.Display(unit)</td>
|
||||||
|
}
|
||||||
|
</tr>
|
||||||
|
}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
<GridColumn Field="@(nameof(UnitData.DpsPerTotalCost))" Title="DpsPerTotalCost" Width="90px" />
|
<div class="d-flex align-items-center gap-2 mt-2">
|
||||||
|
<button type="button" class="btn btn-outline-secondary btn-sm" disabled="@(page <= 1)" @onclick="() => ChangePage(page - 1)">Previous</button>
|
||||||
<GridColumn Field="@(nameof(UnitData.HealthPerTotalCost))" Title="HealthPerTotalCost" Width="90px" />
|
<span class="text-muted">Page @(page) of @TotalPages (@SortedUnits.Count units)</span>
|
||||||
|
<button type="button" class="btn btn-outline-secondary btn-sm" disabled="@(page >= TotalPages)" @onclick="() => ChangePage(page + 1)">Next</button>
|
||||||
<GridColumn Field="@(nameof(UnitData.Supply))" Title="Supply" Width="90px" />
|
|
||||||
<GridColumn Field="@(nameof(UnitData.ProductionTime))" Title="Production Time" Width="90px" />
|
|
||||||
<GridColumn Field="@(nameof(UnitData.Health))" Title="Health" Width="90px" />
|
|
||||||
<GridColumn Field="@(nameof(UnitData.Shields))" Title="Shields" Width="90px" />
|
|
||||||
<GridColumn Field="@(nameof(UnitData.ArmorRating))" Title="Armor Rating" Width="90px" />
|
|
||||||
<GridColumn Field="@(nameof(UnitData.MovementSpeed))" Title="Movement Speed" Width="90px" />
|
|
||||||
<GridColumn Field="@(nameof(UnitData.DamagePerSecond))" Title="Damage Per Second" Width="90px" />
|
|
||||||
<GridColumn Field="@(nameof(UnitData.AttackRange))" Title="Attack Range" Width="90px" />
|
|
||||||
<GridColumn Field="@(nameof(UnitData.Attributes))" Title="Attributes" Width="90px" />
|
|
||||||
<GridColumn Field="@(nameof(UnitData.Tier))" Title="Tier" Width="90px" />
|
|
||||||
<GridColumn Field="@(nameof(UnitData.Faction))" Title="Faction" Width="90px" />
|
|
||||||
<GridColumn Field="@(nameof(UnitData.Hotkey))" Title="Hotkey" Width="90px" />
|
|
||||||
<GridColumn Field="@(nameof(UnitData.BuildAtSameTime))" Title="Build At Same Time" Width="90px" />
|
|
||||||
<GridColumn Field="@(nameof(UnitData.Limit))" Title="Limit" Width="90px" />
|
|
||||||
</GridColumns>
|
|
||||||
</TelerikGrid>
|
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
@code {
|
@code {
|
||||||
private List<UnitData>? gearNotes;
|
private List<UnitData>? units;
|
||||||
private bool isLoading = true;
|
private bool isLoading = true;
|
||||||
private string? errorMessage;
|
private string? errorMessage;
|
||||||
|
|
||||||
|
private string? sortKey;
|
||||||
|
private bool sortAscending = true;
|
||||||
|
private int page = 1;
|
||||||
|
private const int PageSize = 50;
|
||||||
|
private readonly Dictionary<string, string> filters = new();
|
||||||
|
|
||||||
|
private sealed record ColumnDef(
|
||||||
|
string Key,
|
||||||
|
string Title,
|
||||||
|
string Width,
|
||||||
|
Func<UnitData, IComparable?> SortValue,
|
||||||
|
Func<UnitData, string> Display);
|
||||||
|
|
||||||
|
private static readonly IReadOnlyList<ColumnDef> Columns = new List<ColumnDef>
|
||||||
|
{
|
||||||
|
new("Name", "Name", "200px", u => u.Name, u => u.Name),
|
||||||
|
new("Hexite", "Hexite", "90px", u => u.Hexite, u => u.Hexite.ToString()),
|
||||||
|
new("Flux", "Flux", "90px", u => u.Flux, u => u.Flux.ToString()),
|
||||||
|
new("DpsPerTotalCost", "DpsPerTotalCost", "90px", u => u.DpsPerTotalCost, u => u.DpsPerTotalCost.ToString()),
|
||||||
|
new("HealthPerTotalCost", "HealthPerTotalCost", "90px", u => u.HealthPerTotalCost, u => u.HealthPerTotalCost.ToString()),
|
||||||
|
new("Supply", "Supply", "90px", u => u.Supply, u => u.Supply.ToString()),
|
||||||
|
new("ProductionTime", "Production Time", "90px", u => u.ProductionTime, u => u.ProductionTime.ToString()),
|
||||||
|
new("Health", "Health", "90px", u => u.Health, u => u.Health.ToString()),
|
||||||
|
new("Shields", "Shields", "90px", u => u.Shields, u => u.Shields.ToString()),
|
||||||
|
new("ArmorRating", "Armor Rating", "90px", u => u.ArmorRating, u => u.ArmorRating.ToString()),
|
||||||
|
new("MovementSpeed", "Movement Speed", "90px", u => u.MovementSpeed, u => u.MovementSpeed.ToString()),
|
||||||
|
new("DamagePerSecond", "Damage Per Second", "90px", u => u.DamagePerSecond, u => u.DamagePerSecond.ToString()),
|
||||||
|
new("AttackRange", "Attack Range", "90px", u => u.AttackRange, u => u.AttackRange.ToString()),
|
||||||
|
new("Attributes", "Attributes", "90px", u => string.Join(", ", u.Attributes), u => string.Join(", ", u.Attributes)),
|
||||||
|
new("Tier", "Tier", "90px", u => u.Tier, u => u.Tier.ToString()),
|
||||||
|
new("Faction", "Faction", "90px", u => u.Faction, u => u.Faction),
|
||||||
|
new("Hotkey", "Hotkey", "90px", u => u.Hotkey, u => u.Hotkey ?? string.Empty),
|
||||||
|
new("BuildAtSameTime", "Build At Same Time", "90px", u => u.BuildAtSameTime, u => u.BuildAtSameTime.ToString()),
|
||||||
|
new("Limit", "Limit", "90px", u => u.Limit, u => u.Limit?.ToString() ?? string.Empty),
|
||||||
|
};
|
||||||
|
|
||||||
|
private List<UnitData> SortedUnits
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
var query = (units ?? new List<UnitData>()).AsEnumerable();
|
||||||
|
|
||||||
|
foreach (var col in Columns)
|
||||||
|
{
|
||||||
|
if (filters.TryGetValue(col.Key, out var text) && !string.IsNullOrWhiteSpace(text))
|
||||||
|
{
|
||||||
|
query = query.Where(u => col.Display(u).Contains(text, StringComparison.OrdinalIgnoreCase));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var sortCol = Columns.FirstOrDefault(c => c.Key == sortKey);
|
||||||
|
if (sortCol != null)
|
||||||
|
{
|
||||||
|
query = sortAscending
|
||||||
|
? query.OrderBy(u => sortCol.SortValue(u))
|
||||||
|
: query.OrderByDescending(u => sortCol.SortValue(u));
|
||||||
|
}
|
||||||
|
|
||||||
|
return query.ToList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private int TotalPages => Math.Max(1, (int)Math.Ceiling(SortedUnits.Count / (double)PageSize));
|
||||||
|
|
||||||
|
private List<UnitData> PagedUnits => SortedUnits.Skip((page - 1) * PageSize).Take(PageSize).ToList();
|
||||||
|
|
||||||
|
private void ToggleSort(string key)
|
||||||
|
{
|
||||||
|
if (sortKey == key)
|
||||||
|
{
|
||||||
|
sortAscending = !sortAscending;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
sortKey = key;
|
||||||
|
sortAscending = true;
|
||||||
|
}
|
||||||
|
page = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
private string SortGlyph(string key) => sortKey == key ? (sortAscending ? "▲" : "▼") : string.Empty;
|
||||||
|
|
||||||
|
private string GetFilter(string key) => filters.TryGetValue(key, out var value) ? value : string.Empty;
|
||||||
|
|
||||||
|
private void SetFilter(string key, string? value)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(value))
|
||||||
|
{
|
||||||
|
filters.Remove(key);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
filters[key] = value;
|
||||||
|
}
|
||||||
|
page = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ChangePage(int newPage)
|
||||||
|
{
|
||||||
|
page = Math.Clamp(newPage, 1, TotalPages);
|
||||||
|
}
|
||||||
|
|
||||||
protected override async Task OnInitializedAsync()
|
protected override async Task OnInitializedAsync()
|
||||||
{
|
{
|
||||||
await LoadUnitsAsync();
|
await LoadUnitsAsync();
|
||||||
@@ -85,8 +200,8 @@ else
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var units = await UnitsApiClient.GetUnitsAsync();
|
var result = await UnitsApiClient.GetUnitsAsync();
|
||||||
gearNotes = units.ToList();
|
units = result.ToList();
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -24,6 +24,6 @@ builder.Services.AddHttpClient<IUnitsApiClient, UnitsApiClient>(client =>
|
|||||||
client.BaseAddress = new Uri(apiBaseUrl);
|
client.BaseAddress = new Uri(apiBaseUrl);
|
||||||
});
|
});
|
||||||
|
|
||||||
builder.Services.AddTelerikBlazor();
|
builder.Services.AddSingleton<IGalaxyMapExportSerializer, GalaxyMapExportSerializer>();
|
||||||
|
|
||||||
await builder.Build().RunAsync();
|
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);
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
|||||||
@@ -26,22 +26,32 @@ public static class SiteMap
|
|||||||
new(typeof(Pages.Galaxy), "Galaxy Map", "galaxy", "bi bi-galaxy-nav-menu", "MAP",
|
new(typeof(Pages.Galaxy), "Galaxy Map", "galaxy", "bi bi-galaxy-nav-menu", "MAP",
|
||||||
NavLinkMatch.Prefix, Released: true, ShowInNav: true),
|
NavLinkMatch.Prefix, Released: true, ShowInNav: true),
|
||||||
|
|
||||||
|
new(typeof(Pages.PlanetaryMasteryItemIndex), "Planetary Items", "planetary-mastery-items", "bi bi-items-nav-menu", null,
|
||||||
|
NavLinkMatch.All, Released: true, ShowInNav: false),
|
||||||
|
|
||||||
|
new(typeof(Pages.Loadout), "Loadout", "loadout", "bi bi-items-nav-menu", null,
|
||||||
|
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,
|
||||||
NavLinkMatch.Prefix, Released: true, ShowInNav: false),
|
NavLinkMatch.Prefix, Released: true, ShowInNav: false),
|
||||||
|
|
||||||
new(typeof(Pages.SystemMasteryDetail), "Mastery Dossier", "SystemMastery", "bi bi-masteries-nav-menu", null,
|
new(typeof(Pages.SystemMasteryDetail), "Mastery Dossier", "SystemMastery", "bi bi-masteries-nav-menu", null,
|
||||||
NavLinkMatch.Prefix, Released: true, ShowInNav: false),
|
NavLinkMatch.Prefix, Released: true, ShowInNav: false),
|
||||||
|
|
||||||
// In-development pages: hidden from nav and unreachable by URL for now.
|
// In-development pages: hidden from nav and unreachable by URL for now.
|
||||||
new(typeof(Pages.Home), "Home", "", "bi bi-house-door-fill-nav-menu", null,
|
new(typeof(Pages.Home), "Home", "", "bi bi-house-door-fill-nav-menu", null,
|
||||||
NavLinkMatch.All, Released: false, ShowInNav: false),
|
NavLinkMatch.All, Released: false, ShowInNav: true),
|
||||||
new(typeof(Pages.StarSystemIndex), "Star Systems", "StarSystem", "bi bi-star-systems-nav-menu", null,
|
new(typeof(Pages.StarSystemIndex), "Star Systems", "StarSystem", "bi bi-star-systems-nav-menu", null,
|
||||||
NavLinkMatch.Prefix, Released: false, ShowInNav: false),
|
NavLinkMatch.Prefix, Released: false, ShowInNav: true),
|
||||||
new(typeof(Pages.SystemMasteryIndex), "Masteries", "SystemMastery", "bi bi-masteries-nav-menu", null,
|
new(typeof(Pages.SystemMasteryIndex), "Masteries", "SystemMastery", "bi bi-masteries-nav-menu", null,
|
||||||
NavLinkMatch.Prefix, Released: false, ShowInNav: false),
|
NavLinkMatch.Prefix, Released: false, ShowInNav: true),
|
||||||
new(typeof(Pages.Units), "Units", "units", "bi bi-units-nav-menu", null,
|
new(typeof(Pages.Units), "Units", "units", "bi bi-units-nav-menu", null,
|
||||||
NavLinkMatch.Prefix, Released: false, ShowInNav: false),
|
NavLinkMatch.Prefix, Released: false, ShowInNav: true),
|
||||||
];
|
];
|
||||||
|
|
||||||
public static IEnumerable<Entry> NavEntries => Entries.Where(e => e.Released && e.ShowInNav);
|
public static IEnumerable<Entry> NavEntries => Entries.Where(e => e.Released && e.ShowInNav);
|
||||||
|
|||||||
@@ -10,9 +10,6 @@
|
|||||||
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="10.0.9" PrivateAssets="all" />
|
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="10.0.9" PrivateAssets="all" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Http" Version="10.0.12" />
|
<PackageReference Include="Microsoft.Extensions.Http" Version="10.0.12" />
|
||||||
<PackageReference Include="Markdig" Version="1.4.0" />
|
<PackageReference Include="Markdig" Version="1.4.0" />
|
||||||
<PackageReference Include="Telerik.UI.for.Blazor">
|
|
||||||
<Version>13.0.0</Version>
|
|
||||||
</PackageReference>
|
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\ZeroSpace.Model\ZeroSpace.Model.csproj" />
|
<ProjectReference Include="..\ZeroSpace.Model\ZeroSpace.Model.csproj" />
|
||||||
|
|||||||
|
After Width: | Height: | Size: 8.9 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 8.0 KiB |
|
After Width: | Height: | Size: 6.5 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 4.8 KiB |
|
After Width: | Height: | Size: 8.6 KiB |
|
After Width: | Height: | Size: 4.8 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 8.0 KiB |
|
After Width: | Height: | Size: 7.5 KiB |
|
After Width: | Height: | Size: 9.6 KiB |
|
After Width: | Height: | Size: 8.0 KiB |
|
After Width: | Height: | Size: 7.0 KiB |
|
After Width: | Height: | Size: 8.5 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 9.8 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 6.9 KiB |
|
After Width: | Height: | Size: 9.4 KiB |
|
After Width: | Height: | Size: 8.7 KiB |
|
After Width: | Height: | Size: 9.6 KiB |
|
After Width: | Height: | Size: 9.1 KiB |
|
After Width: | Height: | Size: 9.1 KiB |
|
After Width: | Height: | Size: 7.6 KiB |
|
After Width: | Height: | Size: 5.2 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 9.1 KiB |
|
After Width: | Height: | Size: 8.1 KiB |
|
After Width: | Height: | Size: 8.5 KiB |
|
After Width: | Height: | Size: 8.9 KiB |
|
After Width: | Height: | Size: 8.5 KiB |
|
After Width: | Height: | Size: 7.5 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 9.0 KiB |
|
After Width: | Height: | Size: 9.2 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 7.6 KiB |
|
After Width: | Height: | Size: 9.1 KiB |
|
After Width: | Height: | Size: 8.1 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 9.3 KiB |
|
After Width: | Height: | Size: 8.6 KiB |
@@ -10,8 +10,6 @@
|
|||||||
<link rel="stylesheet" href="lib/bootstrap/dist/css/bootstrap.min.css" />
|
<link rel="stylesheet" href="lib/bootstrap/dist/css/bootstrap.min.css" />
|
||||||
<link rel="stylesheet" href="css/app.css" />
|
<link rel="stylesheet" href="css/app.css" />
|
||||||
<link rel="icon" type="image/png" href="favicon.png" />
|
<link rel="icon" type="image/png" href="favicon.png" />
|
||||||
<link href="_content/Telerik.UI.for.Blazor/css/kendo-theme-default/all.css" rel="stylesheet" type="text/css" />
|
|
||||||
|
|
||||||
|
|
||||||
<link href="ZeroSpace.Web.styles.css" rel="stylesheet" />
|
<link href="ZeroSpace.Web.styles.css" rel="stylesheet" />
|
||||||
<script type="importmap"></script>
|
<script type="importmap"></script>
|
||||||
@@ -31,7 +29,7 @@
|
|||||||
<a href="." class="reload">Reload</a>
|
<a href="." class="reload">Reload</a>
|
||||||
<span class="dismiss">🗙</span>
|
<span class="dismiss">🗙</span>
|
||||||
</div>
|
</div>
|
||||||
<script src="_content/Telerik.UI.for.Blazor/js/telerik-blazor.js" defer></script>
|
<script src="js/fileDownload.js"></script>
|
||||||
|
|
||||||
<script src="_framework/blazor.webassembly#[.{fingerprint}].js"></script>
|
<script src="_framework/blazor.webassembly#[.{fingerprint}].js"></script>
|
||||||
</body>
|
</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);
|
||||||
|
};
|
||||||
@@ -36,6 +36,7 @@
|
|||||||
"higherCreditYield": "checkbox",
|
"higherCreditYield": "checkbox",
|
||||||
"unitHealthAndShields": "number",
|
"unitHealthAndShields": "number",
|
||||||
"unitHealthAndShieldRegen": "number",
|
"unitHealthAndShieldRegen": "number",
|
||||||
"availableOn": "multitext"
|
"availableOn": "multitext",
|
||||||
|
"mergeRecipe": "multitext"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
---
|
||||||
|
category: Building
|
||||||
|
hexite: 0
|
||||||
|
Flux: 0
|
||||||
|
productionTime: 35
|
||||||
|
health: 330
|
||||||
|
Armor rating: 0
|
||||||
|
Faction: "[[Xol]]"
|
||||||
|
Tier: 0
|
||||||
|
---
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
---
|
||||||
|
category: Building
|
||||||
|
hexite: 100
|
||||||
|
Flux: 0
|
||||||
|
productionTime: 50
|
||||||
|
health: 1000
|
||||||
|
Armor rating: 1
|
||||||
|
Faction: "[[Xol]]"
|
||||||
|
Tier: 1
|
||||||
|
---
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
---
|
||||||
|
category: Building
|
||||||
|
hexite: 50
|
||||||
|
Flux: 25
|
||||||
|
productionTime: 40
|
||||||
|
health: 1250
|
||||||
|
Armor rating: 1
|
||||||
|
Faction: "[[Xol]]"
|
||||||
|
Tier: 2
|
||||||
|
---
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
---
|
||||||
|
category: Building
|
||||||
|
hexite: 50
|
||||||
|
Flux: 0
|
||||||
|
productionTime: 3
|
||||||
|
health: 250
|
||||||
|
Armor rating: 1
|
||||||
|
Faction: "[[Xol]]"
|
||||||
|
Tier: 0
|
||||||
|
---
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
---
|
||||||
|
category: Building
|
||||||
|
hexite: 150
|
||||||
|
Flux: 0
|
||||||
|
productionTime: 22
|
||||||
|
health: 600
|
||||||
|
Armor rating: 1
|
||||||
|
Faction: "[[Xol]]"
|
||||||
|
---
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
---
|
||||||
|
category: Building
|
||||||
|
hexite: 100
|
||||||
|
Flux: 0
|
||||||
|
productionTime: 50
|
||||||
|
health: 1000
|
||||||
|
Armor rating: 1
|
||||||
|
Faction: "[[Xol]]"
|
||||||
|
Tier: 1
|
||||||
|
---
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
---
|
||||||
|
category: Building
|
||||||
|
hexite: 400
|
||||||
|
Flux: 0
|
||||||
|
productionTime: 60
|
||||||
|
health: 2500
|
||||||
|
Armor rating: 1
|
||||||
|
Faction: "[[Xol]]"
|
||||||
|
Tier: 0
|
||||||
|
---
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
---
|
||||||
|
category: Building
|
||||||
|
hexite: 100
|
||||||
|
Flux: 100
|
||||||
|
productionTime: 75
|
||||||
|
health: 1000
|
||||||
|
Armor rating: 1
|
||||||
|
Faction: "[[Xol]]"
|
||||||
|
---
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
---
|
||||||
|
category: Building
|
||||||
|
hexite: 100
|
||||||
|
Flux: 0
|
||||||
|
productionTime: 50
|
||||||
|
health: 1000
|
||||||
|
Armor rating: 1
|
||||||
|
Faction: "[[Xol]]"
|
||||||
|
Tier: 1
|
||||||
|
---
|
||||||
@@ -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"}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
---
|
||||||
|
category: CoopMercFaction
|
||||||
|
---
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
---
|
||||||
|
category: CoopMercFaction
|
||||||
|
---
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
---
|
||||||
|
category: CoopMercFaction
|
||||||
|
---
|
||||||