317 lines
11 KiB
C#
317 lines
11 KiB
C#
using System.Text;
|
|
using System.Text.Encodings.Web;
|
|
using System.Text.Json;
|
|
using System.Text.RegularExpressions;
|
|
|
|
var exeDir = AppContext.BaseDirectory;
|
|
|
|
var solutionDir = FindContainingDir(exeDir, "ET.sln")
|
|
?? throw new InvalidOperationException("Cannot find ET.sln");
|
|
|
|
var srcDir = Path.GetFullPath(Path.Combine(solutionDir, "..", "docs", "Notes"));
|
|
var dstDir = Path.GetFullPath(Path.Combine(solutionDir, "Web", "wwwroot", "docs", "notes"));
|
|
|
|
Console.WriteLine($"Source: {srcDir}");
|
|
Console.WriteLine($"Destination: {dstDir}");
|
|
|
|
if (!Directory.Exists(srcDir))
|
|
{
|
|
Console.Error.WriteLine($"Source directory not found: {srcDir}");
|
|
return 1;
|
|
}
|
|
|
|
var entries = SyncNotes(srcDir, dstDir);
|
|
CopyOverview(solutionDir, dstDir, entries);
|
|
CopyImages(solutionDir, Path.GetFullPath(Path.Combine(solutionDir, "Web", "wwwroot", "docs")));
|
|
var indexJson = BuildIndex(entries);
|
|
File.WriteAllText(Path.Combine(Path.GetDirectoryName(dstDir)!, "notes-index.json"), indexJson);
|
|
GenerateMap(srcDir, Path.GetFullPath(Path.Combine(solutionDir, "Web", "wwwroot", "docs")));
|
|
|
|
Console.WriteLine("Done.");
|
|
return 0;
|
|
|
|
static List<object> SyncNotes(string srcDir, string dstDir)
|
|
{
|
|
Directory.CreateDirectory(dstDir);
|
|
|
|
foreach (var file in Directory.GetFiles(dstDir))
|
|
File.Delete(file);
|
|
|
|
var entries = new List<object>();
|
|
|
|
foreach (var file in Directory.EnumerateFiles(srcDir, "*.md"))
|
|
{
|
|
var name = Path.GetFileNameWithoutExtension(file);
|
|
var slug = Slugify(name);
|
|
|
|
File.Copy(file, Path.Combine(dstDir, $"{slug}.md"), true);
|
|
|
|
string? category = null;
|
|
string? cost = null;
|
|
string? gearCategory = null;
|
|
string? effect = null;
|
|
string? location = null;
|
|
var content = File.ReadAllText(file);
|
|
var fmMatch = Regex.Match(content, @"^---\s*\n(.*?)\n---", RegexOptions.Singleline);
|
|
if (fmMatch.Success)
|
|
{
|
|
var fm = fmMatch.Groups[1].Value;
|
|
var catMatch = Regex.Match(fm, @"(?m)^category:\s*(.+)$", RegexOptions.IgnoreCase);
|
|
if (catMatch.Success)
|
|
category = catMatch.Groups[1].Value.Trim().Trim('"');
|
|
var costMatch = Regex.Match(fm, @"(?m)^cost:\s*(.+)$", RegexOptions.IgnoreCase);
|
|
if (costMatch.Success)
|
|
cost = costMatch.Groups[1].Value.Trim().Trim('"');
|
|
var gcMatch = Regex.Match(fm, @"(?m)^gear category:\s*(.+)$", RegexOptions.IgnoreCase);
|
|
if (gcMatch.Success)
|
|
gearCategory = gcMatch.Groups[1].Value.Trim().Trim('"');
|
|
var effMatch = Regex.Match(fm, @"(?m)^effect:\s*(.+)$", RegexOptions.IgnoreCase);
|
|
if (effMatch.Success)
|
|
effect = effMatch.Groups[1].Value.Trim().Trim('"');
|
|
var locMatch = Regex.Match(fm, @"(?m)^location:\s*(.+)$", RegexOptions.IgnoreCase);
|
|
if (locMatch.Success)
|
|
location = locMatch.Groups[1].Value.Trim().Trim('"');
|
|
}
|
|
|
|
var entry = new Dictionary<string, object?>
|
|
{
|
|
["slug"] = slug,
|
|
["title"] = name
|
|
};
|
|
if (category != null)
|
|
entry["category"] = category;
|
|
if (cost != null)
|
|
entry["cost"] = cost;
|
|
if (gearCategory != null)
|
|
entry["gearCategory"] = gearCategory;
|
|
if (effect != null)
|
|
entry["effect"] = effect;
|
|
if (location != null)
|
|
entry["location"] = location;
|
|
|
|
entries.Add(entry);
|
|
}
|
|
|
|
Console.WriteLine($"Copied {entries.Count} notes.");
|
|
return entries;
|
|
}
|
|
|
|
static void CopyOverview(string solutionDir, string dstDir, List<object> entries)
|
|
{
|
|
var srcOverview = Path.GetFullPath(Path.Combine(solutionDir, "..", "docs", "Overview.md"));
|
|
if (!File.Exists(srcOverview)) return;
|
|
|
|
var slug = "overview";
|
|
File.Copy(srcOverview, Path.Combine(dstDir, $"{slug}.md"), true);
|
|
|
|
entries.Add(new Dictionary<string, object?>
|
|
{
|
|
["slug"] = slug,
|
|
["title"] = "Overview",
|
|
["category"] = "Overview"
|
|
});
|
|
|
|
Console.WriteLine("Copied Overview.md.");
|
|
}
|
|
|
|
static void CopyImages(string solutionDir, string dstDir)
|
|
{
|
|
var srcImgs = Path.GetFullPath(Path.Combine(solutionDir, "..", "docs", "Images"));
|
|
if (!Directory.Exists(srcImgs)) return;
|
|
|
|
var dstImgs = Path.Combine(dstDir, "images");
|
|
Directory.CreateDirectory(dstImgs);
|
|
|
|
foreach (var file in Directory.EnumerateFiles(srcImgs))
|
|
{
|
|
var name = Path.GetFileName(file);
|
|
File.Copy(file, Path.Combine(dstImgs, name), true);
|
|
}
|
|
|
|
Console.WriteLine($"Copied images from {srcImgs}.");
|
|
}
|
|
|
|
static string BuildIndex(List<object> entries)
|
|
{
|
|
var index = new Dictionary<string, object> { ["notes"] = entries };
|
|
return JsonSerializer.Serialize(index, new JsonSerializerOptions { WriteIndented = true });
|
|
}
|
|
|
|
static void GenerateMap(string srcDir, string dstDir)
|
|
{
|
|
var terrainColors = new Dictionary<string, string>
|
|
{
|
|
["Grass"] = "#4caf50",
|
|
["Forest"] = "#2e7d32",
|
|
["Mountain"] = "#78909c",
|
|
["Water"] = "#42a5f5",
|
|
["Wasteland"] = "#8d6e63"
|
|
};
|
|
|
|
var regionFiles = Directory.EnumerateFiles(srcDir, "*.md")
|
|
.Select(f => (File: f, Content: File.ReadAllText(f)))
|
|
.Where(t => Regex.IsMatch(t.Content, @"(?m)^category:\s*Region\s*$", RegexOptions.Multiline))
|
|
.ToList();
|
|
|
|
var regions = new List<RegionData>();
|
|
|
|
foreach (var (file, content) in regionFiles)
|
|
{
|
|
var fmMatch = Regex.Match(content, @"^---\s*\n(.*?)\n---", RegexOptions.Singleline);
|
|
if (!fmMatch.Success) continue;
|
|
|
|
var fm = fmMatch.Groups[1].Value;
|
|
var name = Path.GetFileNameWithoutExtension(file);
|
|
|
|
var xMatch = Regex.Match(fm, @"(?m)^X:\s*(\d+)");
|
|
var yMatch = Regex.Match(fm, @"(?m)^Y:\s*(\d+)");
|
|
if (!xMatch.Success || !yMatch.Success) continue;
|
|
|
|
var connMatches = Regex.Matches(fm, @"\[\[([^\]]+)\]\]");
|
|
var conns = connMatches.Select(m => m.Groups[1].Value).ToList();
|
|
|
|
var landmarks = new List<string>();
|
|
var lmSection = Regex.Match(fm, @"Landmarks:\s*\n((?:\s*-\s*""\[\[([^\]]+)\]\]""\s*\n?)*)");
|
|
if (lmSection.Success)
|
|
{
|
|
var lmEntries = Regex.Matches(lmSection.Groups[1].Value, @"\[\[([^\]]+)\]\]");
|
|
landmarks.AddRange(lmEntries.Select(m => m.Groups[1].Value));
|
|
}
|
|
|
|
var terrain = name.Split(' ')[0];
|
|
|
|
regions.Add(new RegionData
|
|
{
|
|
Name = name,
|
|
Slug = Slugify(name),
|
|
Terrain = terrain,
|
|
X = int.Parse(xMatch.Groups[1].Value),
|
|
Y = int.Parse(yMatch.Groups[1].Value),
|
|
Connections = conns,
|
|
Landmarks = landmarks
|
|
});
|
|
}
|
|
|
|
|
|
var srcMapImage = Path.GetFullPath(Path.Combine(srcDir, "..", "Images", "Map.png"));
|
|
var dstMapImage = Path.Combine(dstDir, "Map.png");
|
|
if (File.Exists(srcMapImage))
|
|
File.Copy(srcMapImage, dstMapImage, true);
|
|
|
|
var nameLookup = regions.ToDictionary(r => r.Name);
|
|
|
|
var pad = 60;
|
|
var maxX = (regions.Count > 0 ? regions.Max(r => r.X) : 0) + pad * 2;
|
|
var maxY = (regions.Count > 0 ? regions.Max(r => r.Y) : 0) + pad * 2;
|
|
var svg = new StringBuilder();
|
|
svg.AppendLine("<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 700 461\">");
|
|
|
|
svg.AppendLine("""<image href="Map.png" width="700" height="461" preserveAspectRatio="xMidYMid meet"/>""");
|
|
|
|
svg.AppendLine("""<defs>""");
|
|
foreach (var (terrain, color) in terrainColors)
|
|
{
|
|
svg.AppendLine($"""<radialGradient id="glow-{terrain}" cx="50%" cy="50%" r="50%">""");
|
|
svg.AppendLine($"""<stop offset="0%" stop-color="{color}" stop-opacity="0.3"/>""");
|
|
svg.AppendLine($"""<stop offset="100%" stop-color="{color}" stop-opacity="0"/>""");
|
|
svg.AppendLine("</radialGradient>");
|
|
}
|
|
|
|
svg.AppendLine("</defs>");
|
|
|
|
foreach (var region in regions)
|
|
foreach (var conn in region.Connections)
|
|
{
|
|
if (!nameLookup.TryGetValue(conn, out var target) ||
|
|
string.Compare(region.Name, conn, StringComparison.OrdinalIgnoreCase) >= 0)
|
|
continue;
|
|
|
|
svg.AppendLine(
|
|
$"""<line x1="{region.X}" y1="{region.Y}" x2="{target.X}" y2="{target.Y}" stroke="rgba(255,255,255,0.12)" stroke-width="2" stroke-linecap="round"/>""");
|
|
}
|
|
|
|
foreach (var region in regions)
|
|
{
|
|
var cx = region.X;
|
|
var cy = region.Y;
|
|
var color = terrainColors.GetValueOrDefault(region.Terrain, "#888");
|
|
|
|
svg.AppendLine($"""<a href="/docs/{region.Slug}" target="_top">""");
|
|
svg.AppendLine($"""<circle cx="{cx}" cy="{cy}" r="32" fill="url(#glow-{region.Terrain})"/>""");
|
|
svg.AppendLine(
|
|
$"""<circle cx="{cx}" cy="{cy}" r="18" fill="{color}cc" stroke="rgba(255,255,255,0.5)" stroke-width="2"/>""");
|
|
|
|
var labelX = cx + 24;
|
|
svg.AppendLine(
|
|
$"""<text x="{labelX}" y="{cy + 4}" fill="rgba(255,255,255,0.9)" font-family="system-ui,sans-serif" font-size="11" font-weight="600">""");
|
|
svg.Append(HtmlEncoder.Default.Encode(region.Name));
|
|
svg.AppendLine("</text>");
|
|
|
|
foreach (var lm in region.Landmarks)
|
|
{
|
|
svg.AppendLine(
|
|
$"""<text x="{labelX}" y="{cy + 18}" fill="rgba(255,255,255,0.45)" font-family="system-ui,sans-serif" font-size="9" font-style="italic">""");
|
|
svg.Append(HtmlEncoder.Default.Encode($"\u2605 {lm}"));
|
|
svg.AppendLine("</text>");
|
|
}
|
|
|
|
svg.AppendLine("</a>");
|
|
}
|
|
|
|
/**
|
|
var legendX = 700 - 160;
|
|
var legendY = 20;
|
|
svg.AppendLine($"""<rect x="{legendX}" y="{legendY}" width="140" height="{20 + terrainColors.Count * 22}" rx="6" fill="rgba(0,0,0,0.4)" stroke="rgba(255,255,255,0.08)"/>""");
|
|
svg.AppendLine($"""<text x="{legendX + 10}" y="{legendY + 15}" fill="rgba(255,255,255,0.6)" font-family="system-ui,sans-serif" font-size="10" font-weight="600">Legend</text>""");
|
|
var ly = legendY + 32;
|
|
foreach (var (terrain, color) in terrainColors)
|
|
{
|
|
svg.AppendLine($"""<circle cx="{legendX + 14}" cy="{ly - 3}" r="5" fill="{color}"/>""");
|
|
svg.AppendLine($"""<text x="{legendX + 26}" y="{ly + 1}" fill="rgba(255,255,255,0.7)" font-family="system-ui,sans-serif" font-size="10">{terrain}</text>""");
|
|
ly += 22;
|
|
}
|
|
*/
|
|
|
|
svg.AppendLine("</svg>");
|
|
|
|
var mapPath = Path.Combine(dstDir, "map.svg");
|
|
File.WriteAllText(mapPath, svg.ToString());
|
|
Console.WriteLine($"Map written to: {mapPath} ({regions.Count} regions)");
|
|
}
|
|
|
|
static string Slugify(string name)
|
|
{
|
|
var slug = name.ToLowerInvariant()
|
|
.Replace("'", "")
|
|
.Replace(".", "")
|
|
.Replace("(", "")
|
|
.Replace(")", "");
|
|
slug = Regex.Replace(slug, @"[^a-z0-9 -]", "");
|
|
slug = Regex.Replace(slug, @"\s+", "-");
|
|
slug = Regex.Replace(slug, @"-+", "-");
|
|
return slug.Trim('-');
|
|
}
|
|
|
|
static string? FindContainingDir(string startDir, string markerFile)
|
|
{
|
|
var dir = new DirectoryInfo(startDir);
|
|
while (dir != null)
|
|
{
|
|
if (File.Exists(Path.Combine(dir.FullName, markerFile)))
|
|
return dir.FullName;
|
|
dir = dir.Parent;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
internal class RegionData
|
|
{
|
|
public string Name { get; set; } = "";
|
|
public string Slug { get; set; } = "";
|
|
public string Terrain { get; set; } = "";
|
|
public int X { get; set; }
|
|
public int Y { get; set; }
|
|
public List<string> Connections { get; set; } = new();
|
|
public List<string> Landmarks { get; set; } = new();
|
|
} |