From 28f47879f245e4da60d440e4d7feabaf688f594b Mon Sep 17 00:00:00 2001 From: Jonathan McCaffrey Date: Sat, 3 Oct 2026 03:23:23 +0000 Subject: [PATCH] =?UTF-8?q?AI:=20Loadout=20page=20=E2=80=94=20ZeroSpace/Ze?= =?UTF-8?q?roSpace.Build/Program.cs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ZeroSpace/ZeroSpace.Build/Program.cs | 304 ++++++++++++++++++++++++--- 1 file changed, 276 insertions(+), 28 deletions(-) diff --git a/ZeroSpace/ZeroSpace.Build/Program.cs b/ZeroSpace/ZeroSpace.Build/Program.cs index db75d24..0816fea 100644 --- a/ZeroSpace/ZeroSpace.Build/Program.cs +++ b/ZeroSpace/ZeroSpace.Build/Program.cs @@ -72,6 +72,8 @@ if (skipped.Count > 0) var starSystems = new List(); var systemMasteries = new List(); var planetaryMasteryItems = new List(); +var generalItems = new List(); +var factionPowerItems = new List(); var skippedDocs = new List(); var usedIds = new HashSet(StringComparer.Ordinal); @@ -116,55 +118,74 @@ foreach (var path in Directory.EnumerateFiles(docsRoot, "*.md", SearchOption.All 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)); 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 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(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 {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 -// items page can reference them. Images are copied, never moved: the docs folder +// pages can reference them. Images are copied, never moved: the docs folder // stays the source of truth. -var itemImagesDir = Path.Combine(solutionRoot, "ZeroSpace.Web", "wwwroot", "images", "planetary-mastery-items"); -Directory.CreateDirectory(itemImagesDir); +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); -var expectedImages = new HashSet(StringComparer.OrdinalIgnoreCase); -var copiedImages = 0; -foreach (var item in planetaryMasteryItems) -{ - if (item.ImageFileName is null || item.ImageSourcePath is null) - { - continue; - } - - expectedImages.Add(item.ImageFileName); - var destination = Path.Combine(itemImagesDir, item.ImageFileName); - File.Copy(item.ImageSourcePath, destination, overwrite: true); - copiedImages++; -} - -foreach (var stale in Directory.EnumerateFiles(itemImagesDir, "*.png")) -{ - if (!expectedImages.Contains(Path.GetFileName(stale))) - { - File.Delete(stale); - Console.WriteLine($"Removed stale item image: {Path.GetRelativePath(solutionRoot, stale)}"); - } -} - -Console.WriteLine($"Copied {copiedImages} planetary mastery item images to {Path.GetRelativePath(solutionRoot, itemImagesDir)}"); +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) { Console.WriteLine($"Skipped {skippedDocs.Count} incomplete docs:"); @@ -754,6 +775,102 @@ static bool TryReadPlanetaryMasteryItem( return true; } +static bool TryReadGeneralItem( + string path, + IReadOnlyDictionary frontMatter, + string body, + HashSet 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 frontMatter, + string body, + HashSet 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 ReadOptionalLevels(IReadOnlyDictionary frontMatter) +{ + var levels = new List(); + 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) { if (string.Equals(key, "category", StringComparison.OrdinalIgnoreCase) || @@ -896,11 +1013,122 @@ static string GeneratePlanetaryMasteryItems(IReadOnlyList items) +{ + var builder = new StringBuilder(); + builder.AppendLine("// "); + 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 All { get; } ="); + builder.AppendLine(" ["); + + foreach (var item in items) + { + builder.AppendLine($" {item.Id},"); + } + + builder.AppendLine(" ];"); + builder.AppendLine(); + builder.AppendLine(" public static IReadOnlyDictionary ByName { get; } ="); + builder.AppendLine(" All.ToDictionary(static item => item.Name, StringComparer.OrdinalIgnoreCase);"); + builder.AppendLine("}"); + + return builder.ToString(); +} + +static string GenerateFactionPowerItems(IReadOnlyList items) +{ + var builder = new StringBuilder(); + builder.AppendLine("// "); + 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 All { get; } ="); + builder.AppendLine(" ["); + + foreach (var item in items) + { + builder.AppendLine($" {item.Id},"); + } + + builder.AppendLine(" ];"); + builder.AppendLine(); + builder.AppendLine(" public static IReadOnlyDictionary ByName { get; } ="); + builder.AppendLine(" All.ToDictionary(static item => item.Name, StringComparer.OrdinalIgnoreCase);"); + builder.AppendLine("}"); + + return builder.ToString(); +} + static string BoolLiteral(bool value) { return value ? "true" : "false"; } +static int SyncItemImages( + IEnumerable<(string? FileName, string? SourcePath)> items, + string imagesDir, + string solutionRoot) +{ + Directory.CreateDirectory(imagesDir); + + var expectedImages = new HashSet(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 frontMatter, string key) { if (frontMatter.TryGetValue(key, out var value) && @@ -971,3 +1199,23 @@ internal sealed record PlanetaryMasteryItemSource( string? ImageFileName, string? ImageSourcePath, string Body); + +internal sealed record GeneralItemSource( + string Id, + string Name, + IReadOnlyList Levels, + string? LimitedToMerc, + string? ImageFileName, + string? ImageSourcePath, + string Body); + +internal sealed record FactionPowerItemSource( + string Id, + string Name, + IReadOnlyList Levels, + string? LimitedTo, + string? LimitedToMerc, + int? MinimumLevel, + string? ImageFileName, + string? ImageSourcePath, + string Body);