18 Commits

Author SHA1 Message Date
6d486f49 64ac0c41b3 ... 2026-06-11 00:52:30 -04:00
JonathanMcCaffrey 4afb1d6227 ... 2026-06-10 22:42:40 -04:00
JonathanMcCaffrey fd418cb506 ... 2026-06-10 22:40:12 -04:00
JonathanMcCaffrey 436151d642 ... 2026-06-10 22:37:25 -04:00
JonathanMcCaffrey 4b762c168d ... 2026-06-10 22:15:08 -04:00
JonathanMcCaffrey 409489a507 ... 2026-06-10 22:12:02 -04:00
JonathanMcCaffrey 7291c3565f ... 2026-06-10 22:11:00 -04:00
JonathanMcCaffrey 43be7d4ddc ... 2026-06-10 22:06:26 -04:00
JonathanMcCaffrey ca567c65a0 ... 2026-06-10 22:03:35 -04:00
JonathanMcCaffrey bc8d92d06d ... 2026-06-10 22:01:17 -04:00
JonathanMcCaffrey e471875dc3 ... 2026-06-10 21:57:37 -04:00
JonathanMcCaffrey 97ec82dd7f ... 2026-06-10 21:49:39 -04:00
JonathanMcCaffrey d184980586 ... 2026-06-10 21:47:32 -04:00
JonathanMcCaffrey 654ce3e9b0 ... 2026-06-10 21:46:16 -04:00
JonathanMcCaffrey 6bf1b87207 ... 2026-06-10 21:35:28 -04:00
JonathanMcCaffrey 3e80ce78a0 ... 2026-06-10 21:31:37 -04:00
JonathanMcCaffrey 01a49c1c30 ... 2026-06-10 21:22:38 -04:00
JonathanMcCaffrey 3f6dda9602 ... 2026-06-10 21:11:24 -04:00
107 changed files with 46120 additions and 55792 deletions
+30 -169
View File
@@ -1,7 +1,5 @@
using System.Text; using System.Text.RegularExpressions;
using System.Text.Encodings.Web;
using System.Text.Json; using System.Text.Json;
using System.Text.RegularExpressions;
var exeDir = AppContext.BaseDirectory; var exeDir = AppContext.BaseDirectory;
@@ -20,18 +18,13 @@ if (!Directory.Exists(srcDir))
return 1; return 1;
} }
var entries = SyncNotes(srcDir, dstDir); 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"))); GenerateMap(srcDir, Path.GetFullPath(Path.Combine(solutionDir, "Web", "wwwroot", "docs")));
GenerateOverviewPage(solutionDir);
Console.WriteLine("Done."); Console.WriteLine("Done.");
return 0; return 0;
static List<object> SyncNotes(string srcDir, string dstDir) static void SyncNotes(string srcDir, string dstDir)
{ {
Directory.CreateDirectory(dstDir); Directory.CreateDirectory(dstDir);
@@ -45,33 +38,16 @@ static List<object> SyncNotes(string srcDir, string dstDir)
var name = Path.GetFileNameWithoutExtension(file); var name = Path.GetFileNameWithoutExtension(file);
var slug = Slugify(name); var slug = Slugify(name);
File.Copy(file, Path.Combine(dstDir, $"{slug}.md"), true); File.Copy(file, Path.Combine(dstDir, $"{slug}.md"), overwrite: true);
string? category = null; string? category = null;
string? cost = null;
string? gearCategory = null;
string? effect = null;
string? location = null;
var content = File.ReadAllText(file); var content = File.ReadAllText(file);
var fmMatch = Regex.Match(content, @"^---\s*\n(.*?)\n---", RegexOptions.Singleline); var fmMatch = Regex.Match(content, @"^---\s*\n(.*?)\n---", RegexOptions.Singleline);
if (fmMatch.Success) if (fmMatch.Success)
{ {
var fm = fmMatch.Groups[1].Value; var catMatch = Regex.Match(fmMatch.Groups[1].Value, @"(?m)^category:\s*(.+)$");
var catMatch = Regex.Match(fm, @"(?m)^category:\s*(.+)$", RegexOptions.IgnoreCase);
if (catMatch.Success) if (catMatch.Success)
category = catMatch.Groups[1].Value.Trim().Trim('"'); 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?> var entry = new Dictionary<string, object?>
@@ -81,122 +57,18 @@ static List<object> SyncNotes(string srcDir, string dstDir)
}; };
if (category != null) if (category != null)
entry["category"] = category; 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); 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 }; var index = new Dictionary<string, object> { ["notes"] = entries };
return JsonSerializer.Serialize(index, new JsonSerializerOptions { WriteIndented = true }); var json = JsonSerializer.Serialize(index, new JsonSerializerOptions { WriteIndented = true });
}
static void GenerateOverviewPage(string solutionDir) var indexPath = Path.Combine(Path.GetDirectoryName(dstDir)!, "notes-index.json");
{ File.WriteAllText(indexPath, json);
var pagesDir = Path.GetFullPath(Path.Combine(solutionDir, "Web", "Pages"));
Directory.CreateDirectory(pagesDir);
var component = """ Console.WriteLine($"Copied {entries.Count} notes.");
@page "/overview" Console.WriteLine($"Index written to: {indexPath}");
@inject DocsService DocsService
<PageTitle>Overview</PageTitle>
@if (loading)
{
<div class="d-flex justify-content-center py-5">
<div class="spinner-border text-success" role="status">
<span class="visually-hidden">Loading...</span>
</div>
</div>
}
else if (doc == null)
{
<h1>Overview Not Found</h1>
<p>The overview document could not be found.</p>
}
else
{
<div class="section-header d-flex align-items-center mb-4">
<h1 class="mb-0">@doc.Title</h1>
<div class="ms-3 flex-grow-1 border-bottom opacity-25"></div>
</div>
@if (!string.IsNullOrEmpty(doc.FrontmatterHtml))
{
<details class="frontmatter-section" open>
<summary>Frontmatter</summary>
@((MarkupString)doc.FrontmatterHtml)
</details>
}
<div class="markdown-body overview-markdown">
@((MarkupString)doc.HtmlContent)
</div>
}
@code {
private NoteDocument? doc;
private bool loading = true;
protected override async Task OnInitializedAsync()
{
doc = await DocsService.GetNoteAsync("overview");
loading = false;
}
}
""";
var overviewPagePath = Path.Combine(pagesDir, "Overview.razor");
File.WriteAllText(overviewPagePath, component);
Console.WriteLine($"Overview page written to: {overviewPagePath}");
} }
static void GenerateMap(string srcDir, string dstDir) static void GenerateMap(string srcDir, string dstDir)
@@ -207,7 +79,7 @@ static void GenerateMap(string srcDir, string dstDir)
["Forest"] = "#2e7d32", ["Forest"] = "#2e7d32",
["Mountain"] = "#78909c", ["Mountain"] = "#78909c",
["Water"] = "#42a5f5", ["Water"] = "#42a5f5",
["Wasteland"] = "#8d6e63" ["Wasteland"] = "#8d6e63",
}; };
var regionFiles = Directory.EnumerateFiles(srcDir, "*.md") var regionFiles = Directory.EnumerateFiles(srcDir, "*.md")
@@ -254,23 +126,18 @@ static void GenerateMap(string srcDir, string dstDir)
}); });
} }
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 nameLookup = regions.ToDictionary(r => r.Name);
var pad = 60; var pad = 60;
var maxX = (regions.Count > 0 ? regions.Max(r => r.X) : 0) + pad * 2; 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 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"/>"""); var svg = new System.Text.StringBuilder();
svg.AppendLine($"<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 {maxX} {maxY}\">");
svg.AppendLine("""<defs>"""); svg.AppendLine($"""<rect width="100%" height="100%" fill="#1a1a2e" rx="8"/>""");
svg.AppendLine($"""<defs>""");
foreach (var (terrain, color) in terrainColors) foreach (var (terrain, color) in terrainColors)
{ {
svg.AppendLine($"""<radialGradient id="glow-{terrain}" cx="50%" cy="50%" r="50%">"""); svg.AppendLine($"""<radialGradient id="glow-{terrain}" cx="50%" cy="50%" r="50%">""");
@@ -278,50 +145,45 @@ static void GenerateMap(string srcDir, string dstDir)
svg.AppendLine($"""<stop offset="100%" stop-color="{color}" stop-opacity="0"/>"""); svg.AppendLine($"""<stop offset="100%" stop-color="{color}" stop-opacity="0"/>""");
svg.AppendLine("</radialGradient>"); svg.AppendLine("</radialGradient>");
} }
svg.AppendLine("</defs>"); svg.AppendLine("</defs>");
foreach (var region in regions) foreach (var region in regions)
{
foreach (var conn in region.Connections) foreach (var conn in region.Connections)
{ {
if (!nameLookup.TryGetValue(conn, out var target) || if (!nameLookup.TryGetValue(conn, out var target) || string.Compare(region.Name, conn, StringComparison.OrdinalIgnoreCase) >= 0)
string.Compare(region.Name, conn, StringComparison.OrdinalIgnoreCase) >= 0)
continue; continue;
svg.AppendLine( svg.AppendLine($"""<line x1="{region.X + pad}" y1="{region.Y + pad}" x2="{target.X + pad}" y2="{target.Y + pad}" stroke="rgba(255,255,255,0.12)" stroke-width="2" stroke-linecap="round"/>""");
$"""<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) foreach (var region in regions)
{ {
var cx = region.X; var cx = region.X + pad;
var cy = region.Y; var cy = region.Y + pad;
var color = terrainColors.GetValueOrDefault(region.Terrain, "#888"); var color = terrainColors.GetValueOrDefault(region.Terrain, "#888");
svg.AppendLine($"""<a href="/docs/{region.Slug}" target="_top">"""); 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="32" fill="url(#glow-{region.Terrain})"/>""");
svg.AppendLine( svg.AppendLine($"""<circle cx="{cx}" cy="{cy}" r="18" fill="{color}" stroke="rgba(255,255,255,0.4)" stroke-width="2"/>""");
$"""<circle cx="{cx}" cy="{cy}" r="18" fill="{color}cc" stroke="rgba(255,255,255,0.5)" stroke-width="2"/>""");
var labelX = cx + 24; var labelX = cx + 24;
svg.AppendLine( 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">""");
$"""<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(System.Text.Encodings.Web.HtmlEncoder.Default.Encode(region.Name));
svg.Append(HtmlEncoder.Default.Encode(region.Name));
svg.AppendLine("</text>"); svg.AppendLine("</text>");
foreach (var lm in region.Landmarks) foreach (var lm in region.Landmarks)
{ {
svg.AppendLine( 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">""");
$"""<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(System.Text.Encodings.Web.HtmlEncoder.Default.Encode($"\u2605 {lm}"));
svg.Append(HtmlEncoder.Default.Encode($"\u2605 {lm}"));
svg.AppendLine("</text>"); svg.AppendLine("</text>");
} }
svg.AppendLine("</a>"); svg.AppendLine("</a>");
} }
/** var legendX = maxX - 160;
var legendX = 700 - 160;
var legendY = 20; 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($"""<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>"""); 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>""");
@@ -332,7 +194,6 @@ static void GenerateMap(string srcDir, string dstDir)
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>"""); 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; ly += 22;
} }
*/
svg.AppendLine("</svg>"); svg.AppendLine("</svg>");
@@ -363,11 +224,10 @@ static string? FindContainingDir(string startDir, string markerFile)
return dir.FullName; return dir.FullName;
dir = dir.Parent; dir = dir.Parent;
} }
return null; return null;
} }
internal class RegionData class RegionData
{ {
public string Name { get; set; } = ""; public string Name { get; set; } = "";
public string Slug { get; set; } = ""; public string Slug { get; set; } = "";
@@ -377,3 +237,4 @@ internal class RegionData
public List<string> Connections { get; set; } = new(); public List<string> Connections { get; set; } = new();
public List<string> Landmarks { get; set; } = new(); public List<string> Landmarks { get; set; } = new();
} }
+1 -2
View File
@@ -1,5 +1,4 @@
@using Web.Pages <Router AppAssembly="@typeof(App).Assembly" NotFoundPage="typeof(Pages.NotFound)">
<Router AppAssembly="@typeof(App).Assembly" NotFoundPage="typeof(NotFound)">
<Found Context="routeData"> <Found Context="routeData">
<RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)"/> <RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)"/>
<FocusOnNavigate RouteData="@routeData" Selector="h1"/> <FocusOnNavigate RouteData="@routeData" Selector="h1"/>
-2
View File
@@ -5,9 +5,7 @@
</div> </div>
<main> <main>
<article class="content px-4"> <article class="content px-4">
<TelerikRootComponent>
@Body @Body
</TelerikRootComponent>
</article> </article>
</main> </main>
</div> </div>
+1 -2
View File
@@ -9,8 +9,7 @@ main {
} }
.sidebar { .sidebar {
background-color: var(--bg-sidebar); background-image: linear-gradient(180deg, rgb(5, 39, 103) 0%, #3a0647 70%);
border-right: 1px solid var(--border-color);
} }
.top-row { .top-row {
+4 -22
View File
@@ -1,10 +1,8 @@
@inject DocsService DocsService @inject Web.Services.DocsService DocsService
<div class="top-row ps-3 navbar navbar-dark"> <div class="top-row ps-3 navbar navbar-dark">
<div class="container-fluid"> <div class="container-fluid">
<a class="navbar-brand" href=""> <a class="navbar-brand" href="">Web</a>
<span class="brand-text">Trailblazer</span>
</a>
<button title="Navigation menu" class="navbar-toggler" @onclick="ToggleNavMenu"> <button title="Navigation menu" class="navbar-toggler" @onclick="ToggleNavMenu">
<span class="navbar-toggler-icon"></span> <span class="navbar-toggler-icon"></span>
</button> </button>
@@ -18,21 +16,6 @@
<span class="bi bi-house-door-fill-nav-menu" aria-hidden="true"></span> Home <span class="bi bi-house-door-fill-nav-menu" aria-hidden="true"></span> Home
</NavLink> </NavLink>
</div> </div>
<div class="nav-item px-3">
<NavLink class="nav-link" href="overview">
<span class="bi bi-book-nav-menu" aria-hidden="true"></span> Overview
</NavLink>
</div>
<div class="nav-item px-3">
<NavLink class="nav-link" href="gear">
<span class="bi bi-tools-nav-menu" aria-hidden="true"></span> Gear
</NavLink>
</div>
<div class="nav-item px-3">
<NavLink class="nav-link" href="simulation">
<span class="bi bi-graph-up-nav-menu" aria-hidden="true"></span> Simulation
</NavLink>
</div>
@if (groupedNotes == null) @if (groupedNotes == null)
{ {
@@ -70,7 +53,7 @@
protected override async Task OnInitializedAsync() protected override async Task OnInitializedAsync()
{ {
var index = await DocsService.GetIndexAsync(); var index = await DocsService.GetIndexAsync();
groupedNotes = (index.Notes ?? new List<NoteInfo>()) groupedNotes = (index.Notes ?? new())
.GroupBy(n => string.IsNullOrEmpty(n.Category) ? "Uncategorized" : n.Category) .GroupBy(n => string.IsNullOrEmpty(n.Category) ? "Uncategorized" : n.Category)
.OrderBy(g => g.Key) .OrderBy(g => g.Key)
.Select(g => new NoteGroup { Category = g.Key, Notes = g.OrderBy(n => n.Title).ToList() }) .Select(g => new NoteGroup { Category = g.Key, Notes = g.OrderBy(n => n.Title).ToList() })
@@ -80,7 +63,6 @@
private class NoteGroup private class NoteGroup
{ {
public string Category { get; set; } = ""; public string Category { get; set; } = "";
public List<NoteInfo> Notes { get; set; } = new(); public List<Web.Models.NoteInfo> Notes { get; set; } = new();
} }
} }
+22 -40
View File
@@ -9,17 +9,9 @@
} }
.navbar-brand { .navbar-brand {
font-family: 'Outfit', sans-serif; font-size: 1.1rem;
font-size: 1.25rem; font-weight: 600;
font-weight: 700; letter-spacing: 0.02em;
letter-spacing: -0.01em;
color: var(--accent) !important;
}
.brand-text {
background: linear-gradient(135deg, var(--accent) 0%, var(--primary-light) 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
} }
.bi { .bi {
@@ -45,14 +37,6 @@
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-list-nested' viewBox='0 0 16 16'%3E%3Cpath fill-rule='evenodd' d='M4.5 11.5A.5.5 0 0 1 5 11h10a.5.5 0 0 1 0 1H5a.5.5 0 0 1-.5-.5zm-2-4A.5.5 0 0 1 3 7h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5zm-2-4A.5.5 0 0 1 1 3h10a.5.5 0 0 1 0 1H1a.5.5 0 0 1-.5-.5z'/%3E%3C/svg%3E"); background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-list-nested' viewBox='0 0 16 16'%3E%3Cpath fill-rule='evenodd' d='M4.5 11.5A.5.5 0 0 1 5 11h10a.5.5 0 0 1 0 1H5a.5.5 0 0 1-.5-.5zm-2-4A.5.5 0 0 1 3 7h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5zm-2-4A.5.5 0 0 1 1 3h10a.5.5 0 0 1 0 1H1a.5.5 0 0 1-.5-.5z'/%3E%3C/svg%3E");
} }
.bi-book-nav-menu {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-book' viewBox='0 0 16 16'%3E%3Cpath d='M1 2.828c.885-.37 2.154-.769 4-.388A5 5 0 0 1 8 3.03V13.5a.5.5 0 0 1-.5.5c-1.777 0-3.158-.551-4.21-1.11-.534-.284-.822-.702-1.02-1.106C1.879 11.08 1.5 10.183 1.5 9V3c0-.138.172-.255.5-.172zM2 10.304c.367.214.9.482 1.5.662.6.18 1.174.243 1.5.256V3.881c-.563-.064-1.242-.08-2-.354C2.462 3.267 2.184 3.04 2 2.816v7.488zM7.5 13.5V3.03a5 5 0 0 1 2.5-.59c1.846-.38 3.115-.018 4 .388V2.67c0-.138.328-.255.5-.172.328.083.5.255.5.172v7.5c0 .828-.5 1.5-1.5 1.5s-1.5-.672-1.5-1.5v-5.5a.5.5 0 0 0-1 0v5.5c0 .828-.5 1.5-1.5 1.5s-1.5-.672-1.5-1.5v-.5z'/%3E%3C/svg%3E");
}
.bi-tools-nav-menu {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-gear' viewBox='0 0 16 16'%3E%3Cpath d='M8 4.754a3.246 3.246 0 1 0 0 6.492 3.246 3.246 0 0 0 0-6.492zM5.754 8a2.246 2.246 0 1 1 4.492 0 2.246 2.246 0 0 1-4.492 0z'/%3E%3Cpath d='M9.796 1.343c-.527-1.79-3.065-1.79-3.592 0l-.094.319a.873.873 0 0 1-1.255.52l-.292-.16c-1.64-.892-3.433.902-2.54 2.541l.159.292a.873.873 0 0 1-.52 1.255l-.319.094c-1.79.527-1.79 3.065 0 3.592l.319.094a.873.873 0 0 1 .52 1.255l-.16.292c-.892 1.64.901 3.434 2.541 2.54l.292-.159a.873.873 0 0 1 1.255.52l.094.319c.527 1.79 3.065 1.79 3.592 0l.094-.319a.873.873 0 0 1 1.255-.52l.292.16c1.64.893 3.434-.902 2.54-2.541l-.159-.292a.873.873 0 0 1 .52-1.255l.319-.094c1.79-.527 1.79-3.065 0-3.592l-.319-.094a.873.873 0 0 1-.52-1.255l.16-.292c.893-1.64-.902-3.433-2.541-2.54l-.292.159a.873.873 0 0 1-1.255-.52l-.094-.319zm-2.633.283c.246-.835 1.428-.835 1.674 0l.094.319a1.873 1.873 0 0 0 2.693 1.115l.291-.16c.764-.415 1.6.42 1.184 1.185l-.159.292a1.873 1.873 0 0 0 1.116 2.692l.318.094c.835.246.835 1.428 0 1.674l-.319.094a1.873 1.873 0 0 0-1.115 2.693l.16.291c.415.764-.42 1.6-1.185 1.184l-.291-.159a1.873 1.873 0 0 0-2.693 1.116l-.094.318c-.246.835-1.428.835-1.674 0l-.094-.319a1.873 1.873 0 0 0-2.692-1.115l-.292.16c-.764.415-1.6-.42-1.184-1.185l.159-.291A1.873 1.873 0 0 0 1.945 8.93l-.319-.094c-.835-.246-.835-1.428 0-1.674l.319-.094A1.873 1.873 0 0 0 3.06 4.377l-.16-.292c-.415-.764.42-1.6 1.185-1.184l.292.159a1.873 1.873 0 0 0 2.692-1.115l.094-.319z'/%3E%3C/svg%3E");
}
.bi-file-text-nav-menu { .bi-file-text-nav-menu {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-file-text' viewBox='0 0 16 16'%3E%3Cpath d='M5 4a.5.5 0 0 0 0 1h6a.5.5 0 0 0 0-1H5zm-.5 2.5A.5.5 0 0 1 5 6h6a.5.5 0 0 1 0 1H5a.5.5 0 0 1-.5-.5zM5 8a.5.5 0 0 0 0 1h6a.5.5 0 0 0 0-1H5zm0 2a.5.5 0 0 0 0 1h3a.5.5 0 0 0 0-1H5z'/%3E%3Cpath d='M2 2a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V2zm10-1H4a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V2a1 1 0 0 0-1-1z'/%3E%3C/svg%3E"); background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-file-text' viewBox='0 0 16 16'%3E%3Cpath d='M5 4a.5.5 0 0 0 0 1h6a.5.5 0 0 0 0-1H5zm-.5 2.5A.5.5 0 0 1 5 6h6a.5.5 0 0 1 0 1H5a.5.5 0 0 1-.5-.5zM5 8a.5.5 0 0 0 0 1h6a.5.5 0 0 0 0-1H5zm0 2a.5.5 0 0 0 0 1h3a.5.5 0 0 0 0-1H5z'/%3E%3Cpath d='M2 2a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V2zm10-1H4a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V2a1 1 0 0 0-1-1z'/%3E%3C/svg%3E");
} }
@@ -63,42 +47,40 @@
} }
.nav-item ::deep a { .nav-item ::deep a {
color: var(--text-main); color: rgba(255, 255, 255, 0.75);
border-radius: 8px; border-radius: 0;
height: 2.8rem; height: 2.6rem;
display: flex; display: flex;
align-items: center; align-items: center;
line-height: 2.8rem; line-height: 2.6rem;
padding: 0 1rem; padding: 0 1rem;
margin: 0.2rem 0.5rem; border-left: 3px solid transparent;
transition: all 0.2s ease; transition: background-color 0.15s ease, border-color 0.15s ease, color 0.15s ease;
} }
.nav-item ::deep a.active { .nav-item ::deep a.active {
background-color: var(--primary); background-color: rgba(255, 255, 255, 0.12);
color: #fff; color: white;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); border-left-color: rgba(255, 255, 255, 0.7);
} }
.nav-item ::deep a:hover { .nav-item ::deep a:hover {
background-color: rgba(255, 255, 255, 0.05); background-color: rgba(255, 255, 255, 0.07);
color: var(--accent); color: white;
} }
.nav-item-doc ::deep a { .nav-item-doc ::deep a {
padding-left: 2.5rem !important; padding-left: 1.75rem !important;
font-size: 0.85rem; font-size: 0.8rem;
height: 2.2rem !important; height: 1.85rem !important;
line-height: 2.2rem !important; line-height: 1.85rem !important;
margin: 0.1rem 0.5rem; border-left-color: transparent;
} }
.nav-item-doc ::deep a.active { .nav-item-doc ::deep a.active {
background-color: rgba(88, 129, 87, 0.2); background-color: rgba(255, 255, 255, 0.1);
color: var(--accent); color: white;
border-left: 2px solid var(--accent); border-left-color: #6ea8fe;
border-radius: 0 8px 8px 0;
margin-left: 0;
} }
.nav-item-doc ::deep a:hover { .nav-item-doc ::deep a:hover {
-4
View File
@@ -5,10 +5,6 @@ public class NoteInfo
public string Slug { get; set; } = ""; public string Slug { get; set; } = "";
public string Title { get; set; } = ""; public string Title { get; set; } = "";
public string? Category { get; set; } public string? Category { get; set; }
public string? Cost { get; set; }
public string? GearCategory { get; set; }
public string? Effect { get; set; }
public string? Location { get; set; }
} }
public class NotesIndex public class NotesIndex
+2 -3
View File
@@ -1,5 +1,5 @@
@page "/docs/{Slug}" @page "/docs/{Slug}"
@inject DocsService DocsService @inject Web.Services.DocsService DocsService
<PageTitle>@(doc?.Title ?? "Not Found")</PageTitle> <PageTitle>@(doc?.Title ?? "Not Found")</PageTitle>
@@ -39,7 +39,7 @@ else
@code { @code {
[Parameter] public string Slug { get; set; } = ""; [Parameter] public string Slug { get; set; } = "";
private NoteDocument? doc; private Web.Models.NoteDocument? doc;
private bool loading = true; private bool loading = true;
protected override async Task OnParametersSetAsync() protected override async Task OnParametersSetAsync()
@@ -49,5 +49,4 @@ else
doc = await DocsService.GetNoteAsync(Slug); doc = await DocsService.GetNoteAsync(Slug);
loading = false; loading = false;
} }
} }
+3 -38
View File
@@ -1,42 +1,7 @@
@page "/docs" @page "/docs"
@inject DocsService DocsService
<PageTitle>Documentation</PageTitle> <PageTitle>Docs</PageTitle>
<div class="section-header d-flex align-items-center mb-4"> <h1>Documentation</h1>
<h1 class="mb-0">Documentation</h1>
<div class="ms-3 flex-grow-1 border-bottom opacity-25"></div>
</div>
@if (index == null) <p>Select a note from the sidebar to view its contents.</p>
{
<div class="d-flex justify-content-center py-5">
<div class="spinner-border text-success" role="status">
<span class="visually-hidden">Loading...</span>
</div>
</div>
}
else
{
<div class="docs-grid">
@foreach (var note in index.Notes.OrderBy(n => n.Title))
{
<NavLink href="@($"docs/{note.Slug}")" class="docs-card">
<div class="d-flex justify-content-between align-items-start mb-2">
<span class="badge bg-dark text-success border border-success border-opacity-25">@note.Category</span>
</div>
<h3>@note.Title</h3>
<p class="text-muted small mb-0">Explore details about @note.Title</p>
</NavLink>
}
</div>
}
@code {
private NotesIndex? index;
protected override async Task OnInitializedAsync()
{
index = await DocsService.GetIndexAsync();
}
}
-51
View File
@@ -1,51 +0,0 @@
@page "/gear"
@inject DocsService DocsService
<PageTitle>Gear & Equipment</PageTitle>
<div class="section-header d-flex align-items-center mb-4">
<h1 class="mb-0">Gear & Equipment</h1>
<div class="ms-3 flex-grow-1 border-bottom opacity-25"></div>
</div>
@if (gearNotes == null)
{
<div class="d-flex justify-content-center py-5">
<div class="spinner-border text-success" role="status">
<span class="visually-hidden">Loading...</span>
</div>
</div>
}
else
{
<div class="grid-container">
<TelerikGrid Data="@gearNotes" Pageable="true" PageSize="50" Sortable="true" FilterMode="@GridFilterMode.FilterRow"
Height="calc(100vh - 250px)">
<GridColumns>
<GridColumn Field="@(nameof(NoteInfo.Title))" Title="Item Name" Width="200px">
<Template>
<NavLink href="@($"docs/{(context as NoteInfo)!.Slug}")" class="fw-bold">@((context as NoteInfo)!.Title)</NavLink>
</Template>
</GridColumn>
<GridColumn Field="@(nameof(NoteInfo.Cost))" Title="Cost" Width="90px" />
<GridColumn Field="@(nameof(NoteInfo.GearCategory))" Title="Category" Width="140px"/>
<GridColumn Field="@(nameof(NoteInfo.Effect))" Title="Effect"/>
<GridColumn Field="@(nameof(NoteInfo.Location))" Title="Acquisition" Width="150px"/>
</GridColumns>
</TelerikGrid>
</div>
}
@code {
private List<NoteInfo>? gearNotes;
protected override async Task OnInitializedAsync()
{
var index = await DocsService.GetIndexAsync();
gearNotes = index.Notes
.Where(n => string.Equals(n.Category, "Gear", StringComparison.OrdinalIgnoreCase))
.OrderBy(n => n.Title)
.ToList();
}
}
+2 -16
View File
@@ -2,22 +2,8 @@
<PageTitle>Earthborne Trailblazer</PageTitle> <PageTitle>Earthborne Trailblazer</PageTitle>
<div class="hero-section text-center"> <h1>Earthborne Trailblazer</h1>
<div class="hero-content py-5">
<h1 class="display-4 mb-3">Earthborne Trailblazer</h1>
<p class="lead mb-4">Your essential companion guide for navigating the Valley and mastering your craft.</p>
<div class="hero-actions">
<NavLink href="overview" class="btn btn-primary btn-lg px-4">Begin Journey</NavLink>
</div>
</div>
</div>
<div class="container-fluid px-0 mt-4"> <div class="map-container">
<div class="section-header d-flex align-items-center mb-3">
<h2 class="h4 mb-0">Valley Map</h2>
<div class="ms-3 flex-grow-1 border-bottom opacity-25"></div>
</div>
<div class="map-container shadow-lg">
<object data="docs/map.svg" type="image/svg+xml" class="map-svg"></object> <object data="docs/map.svg" type="image/svg+xml" class="map-svg"></object>
</div> </div>
</div>
+4 -15
View File
@@ -1,16 +1,5 @@
@page "/404" @page "/not-found"
@page "/not-found" @layout MainLayout
<PageTitle>404 - Not Found</PageTitle> <h3>Not Found</h3>
<p>Sorry, the content you are looking for does not exist.</p>
<div class="d-flex flex-column align-items-center justify-content-center py-5 text-center">
<div class="mb-4">
<i class="bi bi-exclamation-triangle text-warning" style="font-size: 4rem;"></i>
</div>
<h1 class="display-4 mb-3">404</h1>
<h2 class="mb-4">Path Not Found</h2>
<p class="lead mb-5 text-muted">The trail you are following seems to have vanished into the wilderness.</p>
<NavLink href="" class="btn btn-primary px-4">
Return to Safety
</NavLink>
</div>
-48
View File
@@ -1,48 +0,0 @@
@page "/overview"
@inject DocsService DocsService
<PageTitle>Overview</PageTitle>
@if (loading)
{
<div class="d-flex justify-content-center py-5">
<div class="spinner-border text-success" role="status">
<span class="visually-hidden">Loading...</span>
</div>
</div>
}
else if (doc == null)
{
<h1>Overview Not Found</h1>
<p>The overview document could not be found.</p>
}
else
{
<div class="section-header d-flex align-items-center mb-4">
<h1 class="mb-0">@doc.Title</h1>
<div class="ms-3 flex-grow-1 border-bottom opacity-25"></div>
</div>
@if (!string.IsNullOrEmpty(doc.FrontmatterHtml))
{
<details class="frontmatter-section" open>
<summary>Frontmatter</summary>
@((MarkupString)doc.FrontmatterHtml)
</details>
}
<div class="markdown-body overview-markdown">
@((MarkupString)doc.HtmlContent)
</div>
}
@code {
private NoteDocument? doc;
private bool loading = true;
protected override async Task OnInitializedAsync()
{
doc = await DocsService.GetNoteAsync("overview");
loading = false;
}
}
-357
View File
@@ -1,357 +0,0 @@
@page "/simulation"
@inject GameSimulationService SimService
<PageTitle>Ecology Simulation</PageTitle>
<div class="section-header d-flex align-items-center mb-4">
<h1 class="mb-0">Ecology Simulation</h1>
<div class="ms-3 flex-grow-1 border-bottom opacity-25"></div>
</div>
@if (!SimService.Data.IsInitialized)
{
<div class="text-center py-5">
<p class="lead mb-4">Simulate 20 turns of predator, prey, and flora ecology across the valley.</p>
<button class="btn btn-primary btn-lg" @onclick="StartSimulation">Run Simulation</button>
</div>
}
else
{
<div class="simulation-layout" tabindex="0" @onkeydown="HandleKeyDown" @ref="layoutDiv">
<div class="sim-controls d-flex align-items-center justify-content-between mb-3 flex-wrap gap-2">
<div class="d-flex align-items-center gap-2">
<button class="btn btn-outline-light btn-sm" @onclick="PrevTurn" disabled="@(SimService.Data.CurrentTurn <= 0)">
&#9664; Prev
</button>
<span class="turn-label">
@if (SimService.Data.CurrentTurn == 0)
{
<span>Initial Setup</span>
}
else
{
<span>After Turn <strong>@SimService.Data.CurrentTurn</strong> / 20</span>
}
</span>
<button class="btn btn-outline-light btn-sm" @onclick="NextTurn" disabled="@(SimService.Data.CurrentTurn >= 20)">
Next &#9654;
</button>
</div>
<div class="d-flex align-items-center gap-2 flex-wrap">
@if (CurrentEvent != null)
{
@foreach (var act in CurrentEvent.Activations)
{
<span class="event-badge badge bg-@BadgeClass(act.MeepleType)">
@act.MeepleType in @act.RegionType
</span>
}
}
<button class="btn btn-secondary btn-sm" @onclick="RandomizeEvents">&#8635; Randomize Events</button>
</div>
</div>
<div class="sim-map-container">
<svg viewBox="0 0 700 461" class="sim-svg">
<defs>
<radialGradient id="sg-Grass" cx="50%" cy="50%" r="50%">
<stop offset="0%" stop-color="#4caf50" stop-opacity="0.3"/>
<stop offset="100%" stop-color="#4caf50" stop-opacity="0"/>
</radialGradient>
<radialGradient id="sg-Forest" cx="50%" cy="50%" r="50%">
<stop offset="0%" stop-color="#2e7d32" stop-opacity="0.3"/>
<stop offset="100%" stop-color="#2e7d32" stop-opacity="0"/>
</radialGradient>
<radialGradient id="sg-Mountain" cx="50%" cy="50%" r="50%">
<stop offset="0%" stop-color="#78909c" stop-opacity="0.3"/>
<stop offset="100%" stop-color="#78909c" stop-opacity="0"/>
</radialGradient>
<radialGradient id="sg-Water" cx="50%" cy="50%" r="50%">
<stop offset="0%" stop-color="#42a5f5" stop-opacity="0.3"/>
<stop offset="100%" stop-color="#42a5f5" stop-opacity="0"/>
</radialGradient>
<radialGradient id="sg-Wasteland" cx="50%" cy="50%" r="50%">
<stop offset="0%" stop-color="#8d6e63" stop-opacity="0.3"/>
<stop offset="100%" stop-color="#8d6e63" stop-opacity="0"/>
</radialGradient>
</defs>
<image href="docs/Map.png" width="700" height="461" preserveAspectRatio="xMidYMid meet"/>
@{
var currentRegions = SimService.GetCurrentState();
var prevRegions = SimService.GetPreviousState();
var prevLookup = prevRegions.ToDictionary(r => r.Name);
var activatedRegionNames = new HashSet<string>();
if (CurrentEvent != null)
{
var activatedTerrain = CurrentEvent.Activations.Select(a => a.RegionType).ToHashSet();
activatedRegionNames = currentRegions
.Where(r => activatedTerrain.Contains(r.Terrain))
.Select(r => r.Name)
.ToHashSet();
}
}
@foreach (var line in GetConnectionLines(currentRegions))
{
<line x1="@line.X1" y1="@line.Y1" x2="@line.X2" y2="@line.Y2"
stroke="rgba(255,255,255,0.12)" stroke-width="2" stroke-linecap="round"/>
}
@foreach (var region in currentRegions)
{
var isActivated = activatedRegionNames.Contains(region.Name);
var terrainColor = GetTerrainColor(region.Terrain);
var labelX = region.X + 24;
var prev = prevLookup.GetValueOrDefault(region.Name);
var dp = prev != null ? region.PredatorMeeples - prev.PredatorMeeples : 0;
var dr = prev != null ? region.PreyMeeples - prev.PreyMeeples : 0;
var df = prev != null ? region.FloraMeeples - prev.FloraMeeples : 0;
var hasDelta = dp != 0 || dr != 0 || df != 0;
// Meeple dot counts (max 3 visible per type)
var pdots = Math.Min(region.PredatorMeeples, 3);
var rdots = Math.Min(region.PreyMeeples, 3);
var fdots = Math.Min(region.FloraMeeples, 3);
var dotR = 3;
var dotGap = 7;
<g class="sim-region @(isActivated ? "sim-active" : "")">
<circle cx="@region.X" cy="@region.Y" r="34" fill="url(#sg-@region.Terrain)"/>
<circle cx="@region.X" cy="@region.Y" r="18"
fill="@(terrainColor)cc"
stroke="@(isActivated ? "#ffd700" : "rgba(255,255,255,0.5)")"
stroke-width="@(isActivated ? 3 : 2)"/>
@* Predator dots (red, top row) *@
@for (int d = 0; d < pdots; d++)
{
var dx = region.X + (d - (pdots - 1) * 0.5) * dotGap;
var dy = region.Y - 7;
<circle cx="@dx" cy="@dy" r="@dotR" fill="#e76f51" stroke="rgba(0,0,0,0.4)" stroke-width="0.5"/>
}
@if (region.PredatorMeeples > 3)
{
@((MarkupString)$"<text x=\"{region.X + 2 * dotGap + 2}\" y=\"{region.Y - 4}\" fill=\"#e76f51\" font-size=\"7\" font-weight=\"600\">+{region.PredatorMeeples - 3}</text>")
}
@* Prey dots (yellow, middle row) *@
@for (int d = 0; d < rdots; d++)
{
var dx = region.X + (d - (rdots - 1) * 0.5) * dotGap;
var dy = region.Y;
<circle cx="@dx" cy="@dy" r="@dotR" fill="#e9c46a" stroke="rgba(0,0,0,0.4)" stroke-width="0.5"/>
}
@if (region.PreyMeeples > 3)
{
@((MarkupString)$"<text x=\"{region.X + 2 * dotGap + 2}\" y=\"{region.Y + 3}\" fill=\"#e9c46a\" font-size=\"7\" font-weight=\"600\">+{region.PreyMeeples - 3}</text>")
}
@* Flora dots (green, bottom row) *@
@for (int d = 0; d < fdots; d++)
{
var dx = region.X + (d - (fdots - 1) * 0.5) * dotGap;
var dy = region.Y + 7;
<circle cx="@dx" cy="@dy" r="@dotR" fill="#4caf50" stroke="rgba(0,0,0,0.4)" stroke-width="0.5"/>
}
@if (region.FloraMeeples > 3)
{
@((MarkupString)$"<text x=\"{region.X + 2 * dotGap + 2}\" y=\"{region.Y + 10}\" fill=\"#4caf50\" font-size=\"7\" font-weight=\"600\">+{region.FloraMeeples - 3}</text>")
}
<text x="@labelX" y="@(region.Y + 4)" fill="rgba(255,255,255,0.9)"
font-family="system-ui,sans-serif" font-size="11" font-weight="600">
@region.Name
</text>
<text x="@labelX" y="@(region.Y + 20)" fill="rgba(255,255,255,0.7)"
font-family="system-ui,sans-serif" font-size="10">
P:@region.PredatorMeeples R:@region.PreyMeeples F:@region.FloraMeeples
</text>
@if (hasDelta && SimService.Data.CurrentTurn > 0)
{
var dy = region.Y + 34;
var parts = new List<string>();
if (dp != 0) parts.Add($"<tspan fill=\"{(dp > 0 ? "#4caf50" : "#e76f51")}\">{(dp > 0 ? "+" : "")}{dp}P</tspan>");
if (dr != 0) parts.Add($"<tspan fill=\"{(dr > 0 ? "#4caf50" : "#e76f51")}\">{(dr > 0 ? "+" : "")}{dr}R</tspan>");
if (df != 0) parts.Add($"<tspan fill=\"{(df > 0 ? "#4caf50" : "#e76f51")}\">{(df > 0 ? "+" : "")}{df}F</tspan>");
var deltaHtml = $"<text x=\"{labelX}\" y=\"{dy}\" font-family=\"system-ui,sans-serif\" font-size=\"9\">{string.Join(" ", parts)}</text>";
@((MarkupString)deltaHtml)
}
</g>
}
</svg>
</div>
@if (CurrentEvent != null && CurrentEvent.Details.Count > 0)
{
<div class="sim-details mt-3">
<h5 class="text-muted mb-2">Turn @CurrentEvent.TurnNumber Details</h5>
<div class="sim-details-scroll">
@foreach (var detail in CurrentEvent.Details)
{
<div class="sim-detail-item">@detail</div>
}
</div>
</div>
}
<div class="sim-totals mt-3 d-flex gap-4 justify-content-center">
@{
var totals = SimService.GetCurrentState();
}
<div class="total-pred"><span class="total-dot" style="background:#e76f51"></span> Predators: @totals.Sum(r => r.PredatorMeeples)</div>
<div class="total-prey"><span class="total-dot" style="background:#e9c46a"></span> Prey: @totals.Sum(r => r.PreyMeeples)</div>
<div class="total-flora"><span class="total-dot" style="background:#4caf50"></span> Flora: @totals.Sum(r => r.FloraMeeples)</div>
</div>
<div class="sim-chart mt-3">
<h5 class="text-muted mb-2">Population Trend</h5>
<svg viewBox="0 0 720 160" class="sim-chart-svg">
@{
var history = SimService.GetPopulationHistory();
var maxPop = Math.Max(history.Max(h => h.predators + h.prey + h.flora), 10);
var chartW = 700d;
var chartH = 120d;
var chartY = 20d;
var stepX = history.Length > 1 ? chartW / (history.Length - 1) : chartW;
string MakePath(Func<int, int> getVal, string color)
{
var pts = history.Select((h, i) =>
{
var x = i * stepX;
var y = chartY + chartH - (getVal(i) / (double)maxPop) * chartH;
return $"{x:F1},{y:F1}";
});
var d = "M" + string.Join(" L", pts);
return $"<path d=\"{d}\" fill=\"none\" stroke=\"{color}\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>";
}
// Y axis labels
@((MarkupString)$"<text x=\"0\" y=\"{chartY + 10}\" fill=\"rgba(255,255,255,0.3)\" font-size=\"9\">{maxPop}</text>")
@((MarkupString)$"<text x=\"0\" y=\"{chartY + chartH}\" fill=\"rgba(255,255,255,0.3)\" font-size=\"9\">0</text>")
// Grid lines
<line x1="0" y1="@(chartY + chartH)" x2="@chartW" y2="@(chartY + chartH)" stroke="rgba(255,255,255,0.06)" stroke-width="1"/>
<line x1="0" y1="@chartY" x2="@chartW" y2="@chartY" stroke="rgba(255,255,255,0.06)" stroke-width="1"/>
// Current turn marker
var markerX = SimService.Data.CurrentTurn * stepX;
<line x1="@markerX" y1="@chartY" x2="@markerX" y2="@(chartY + chartH)" stroke="rgba(255,255,255,0.25)" stroke-width="1" stroke-dasharray="3,3"/>
@((MarkupString)MakePath(i => history[i].predators, "#e76f51"))
@((MarkupString)MakePath(i => history[i].prey, "#e9c46a"))
@((MarkupString)MakePath(i => history[i].flora, "#4caf50"))
// Turn markers on x-axis
@for (int i = 0; i < history.Length; i += 5)
{
var x = i * stepX;
@((MarkupString)$"<text x=\"{x}\" y=\"{chartY + chartH + 14}\" fill=\"rgba(255,255,255,0.25)\" font-size=\"8\" text-anchor=\"middle\">{i}</text>")
}
}
</svg>
<div class="sim-chart-legend d-flex gap-3 justify-content-center mt-1">
<span class="small"><span class="total-dot" style="background:#e76f51"></span> Predators</span>
<span class="small"><span class="total-dot" style="background:#e9c46a"></span> Prey</span>
<span class="small"><span class="total-dot" style="background:#4caf50"></span> Flora</span>
</div>
</div>
</div>
}
@code {
private ElementReference layoutDiv;
private TurnEvent? CurrentEvent => SimService.Data.CurrentTurn > 0 && SimService.Data.CurrentTurn <= SimService.Data.Events.Count
? SimService.Data.Events[SimService.Data.CurrentTurn - 1]
: null;
private static string BadgeClass(string meepleType) => meepleType switch
{
"Predator" => "danger",
"Prey" => "warning",
"Flora" => "success",
_ => "secondary"
};
protected override void OnInitialized()
{
SimService.RunSimulation();
}
private void StartSimulation()
{
SimService.RunSimulation();
}
private void PrevTurn()
{
if (SimService.Data.CurrentTurn > 0)
SimService.Data.CurrentTurn--;
}
private void NextTurn()
{
if (SimService.Data.CurrentTurn < 20)
SimService.Data.CurrentTurn++;
}
private void HandleKeyDown(KeyboardEventArgs e)
{
if (e.Key == "ArrowLeft") PrevTurn();
else if (e.Key == "ArrowRight") NextTurn();
}
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
await layoutDiv.FocusAsync();
}
private void RandomizeEvents()
{
SimService.RandomizeEvents();
}
private static string GetTerrainColor(string terrain)
{
return terrain switch
{
"Grass" => "#4caf50",
"Forest" => "#2e7d32",
"Mountain" => "#78909c",
"Water" => "#42a5f5",
"Wasteland" => "#8d6e63",
_ => "#888"
};
}
private record ConnectionLine(int X1, int Y1, int X2, int Y2);
private List<ConnectionLine> GetConnectionLines(List<RegionState> regions)
{
var lookup = regions.ToDictionary(r => r.Name);
var lines = new List<ConnectionLine>();
var drawn = new HashSet<string>();
foreach (var region in regions)
{
foreach (var conn in region.Connections)
{
var key = string.Compare(region.Name, conn, StringComparison.OrdinalIgnoreCase) < 0
? $"{region.Name}|{conn}"
: $"{conn}|{region.Name}";
if (!drawn.Add(key)) continue;
if (!lookup.TryGetValue(conn, out var target)) continue;
lines.Add(new ConnectionLine(region.X, region.Y, target.X, target.Y));
}
}
return lines;
}
}
-3
View File
@@ -9,8 +9,5 @@ builder.RootComponents.Add<HeadOutlet>("head::after");
builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) }); builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
builder.Services.AddScoped<DocsService>(); builder.Services.AddScoped<DocsService>();
builder.Services.AddSingleton<GameSimulationService>();
builder.Services.AddTelerikBlazor();
await builder.Build().RunAsync(); await builder.Build().RunAsync();
+17 -85
View File
@@ -1,7 +1,4 @@
using System.Net;
using System.Net.Http.Json; using System.Net.Http.Json;
using System.Text;
using System.Text.RegularExpressions;
using Markdig; using Markdig;
using Web.Models; using Web.Models;
@@ -9,19 +6,11 @@ namespace Web.Services;
public class DocsService public class DocsService
{ {
private static readonly MarkdownPipeline Pipeline = new MarkdownPipelineBuilder()
.UseYamlFrontMatter()
.UsePipeTables()
.Build();
private static readonly HashSet<string> ImageExtensions = new(StringComparer.OrdinalIgnoreCase)
{
".png", ".jpg", ".jpeg", ".gif", ".svg", ".webp", ".bmp"
};
private readonly HttpClient _http; private readonly HttpClient _http;
private NotesIndex? _index; private NotesIndex? _index;
private readonly Dictionary<string, string> _markdownCache = new(); private static readonly MarkdownPipeline Pipeline = new MarkdownPipelineBuilder()
.UseYamlFrontMatter()
.Build();
public DocsService(HttpClient http) public DocsService(HttpClient http)
{ {
@@ -49,7 +38,7 @@ public class DocsService
try try
{ {
var markdown = await _http.GetStringAsync($"docs/notes/{slug}.md"); var markdown = await _http.GetStringAsync($"docs/notes/{slug}.md");
return await ParseDocument(slug, noteInfo.Title, markdown); return ParseDocument(slug, noteInfo.Title, markdown);
} }
catch catch
{ {
@@ -57,7 +46,7 @@ public class DocsService
} }
} }
private async Task<NoteDocument> ParseDocument(string slug, string title, string markdown, int depth = 0) private static NoteDocument ParseDocument(string slug, string title, string markdown)
{ {
var doc = new NoteDocument var doc = new NoteDocument
{ {
@@ -80,7 +69,6 @@ public class DocsService
inFrontmatter = true; inFrontmatter = true;
continue; continue;
} }
inFrontmatter = false; inFrontmatter = false;
frontmatterDone = true; frontmatterDone = true;
continue; continue;
@@ -101,11 +89,13 @@ public class DocsService
{ {
var key = line[..colonIdx].Trim(); var key = line[..colonIdx].Trim();
if (string.Equals(key, "category", StringComparison.OrdinalIgnoreCase)) if (string.Equals(key, "category", StringComparison.OrdinalIgnoreCase))
{
doc.Category = line[(colonIdx + 1)..].Trim().Trim('"'); doc.Category = line[(colonIdx + 1)..].Trim().Trim('"');
} }
} }
}
var fmHtml = new StringBuilder(); var fmHtml = new System.Text.StringBuilder();
fmHtml.Append("<table class=\"frontmatter\">"); fmHtml.Append("<table class=\"frontmatter\">");
foreach (var line in frontmatterLines) foreach (var line in frontmatterLines)
{ {
@@ -115,93 +105,35 @@ public class DocsService
var key = line[..colonIdx].Trim(); var key = line[..colonIdx].Trim();
var value = line[(colonIdx + 1)..].Trim().Trim('"'); var value = line[(colonIdx + 1)..].Trim().Trim('"');
fmHtml.Append("<tr><td class=\"fm-key\">"); fmHtml.Append("<tr><td class=\"fm-key\">");
fmHtml.Append(WebUtility.HtmlEncode(key)); fmHtml.Append(System.Net.WebUtility.HtmlEncode(key));
fmHtml.Append("</td><td class=\"fm-value\">"); fmHtml.Append("</td><td class=\"fm-value\">");
var encoded = WebUtility.HtmlEncode(value); var encoded = System.Net.WebUtility.HtmlEncode(value);
fmHtml.Append(ConvertWikiLinks(encoded)); fmHtml.Append(ConvertWikiLinks(encoded));
fmHtml.Append("</td></tr>"); fmHtml.Append("</td></tr>");
} }
else else
{ {
fmHtml.Append("<tr><td colspan=\"2\">"); fmHtml.Append("<tr><td colspan=\"2\">");
fmHtml.Append(ConvertWikiLinks(WebUtility.HtmlEncode(line.Trim()))); fmHtml.Append(ConvertWikiLinks(System.Net.WebUtility.HtmlEncode(line.Trim())));
fmHtml.Append("</td></tr>"); fmHtml.Append("</td></tr>");
} }
} }
fmHtml.Append("</table>"); fmHtml.Append("</table>");
doc.FrontmatterHtml = fmHtml.ToString(); doc.FrontmatterHtml = fmHtml.ToString();
} }
var body = string.Join("\n", bodyLines); var body = string.Join("\n", bodyLines);
body = ConvertWikiLinks(body); body = ConvertWikiLinks(body);
var html = Markdown.ToHtml(body, Pipeline); doc.HtmlContent = Markdown.ToHtml(body, Pipeline);
html = await ResolveEmbedsInHtml(html, depth);
doc.HtmlContent = html;
return doc; return doc;
} }
private async Task<string> ResolveEmbedsInHtml(string html, int depth)
{
if (depth > 10) return html;
var regex = new Regex(@"(?:<p>)?!\[\[([^\]]+)\]\](?:</p>)?");
var sb = new StringBuilder();
var lastIndex = 0;
foreach (Match match in regex.Matches(html))
{
sb.Append(html, lastIndex, match.Index - lastIndex);
var filename = match.Groups[1].Value.Trim();
var replacement = await ResolveEmbed(filename, depth);
sb.Append(replacement);
lastIndex = match.Index + match.Length;
}
sb.Append(html, lastIndex, html.Length - lastIndex);
return sb.ToString();
}
private async Task<string> ResolveEmbed(string filename, int depth)
{
var ext = Path.GetExtension(filename);
if (ImageExtensions.Contains(ext))
{
return $"<img src=\"/docs/images/{filename}\" alt=\"{WebUtility.HtmlEncode(filename)}\" />";
}
var slug = Slugify(filename);
try
{
var markdown = await GetMarkdownAsync(slug);
var embedded = await ParseDocument(slug, filename, markdown, depth + 1);
return $"<div class=\"embed\">{embedded.HtmlContent}</div>";
}
catch
{
return $"<div class=\"embed-error\">[Embed not found: {WebUtility.HtmlEncode(filename)}]</div>";
}
}
private async Task<string> GetMarkdownAsync(string slug)
{
if (_markdownCache.TryGetValue(slug, out var cached))
return cached;
var markdown = await _http.GetStringAsync($"docs/notes/{slug}.md");
_markdownCache[slug] = markdown;
return markdown;
}
private static string ConvertWikiLinks(string text) private static string ConvertWikiLinks(string text)
{ {
return Regex.Replace( return System.Text.RegularExpressions.Regex.Replace(
text, text,
@"(?<!!)\[\[([^\]]+)\]\]", @"\[\[([^\]]+)\]\]",
match => match =>
{ {
var content = match.Groups[1].Value; var content = match.Groups[1].Value;
@@ -209,7 +141,7 @@ public class DocsService
var linkText = parts.Length > 1 ? parts[1].Trim() : parts[0].Trim(); var linkText = parts.Length > 1 ? parts[1].Trim() : parts[0].Trim();
var target = parts[0].Trim(); var target = parts[0].Trim();
var slug = Slugify(target); var slug = Slugify(target);
return $"<a href=\"/docs/{slug}\">{WebUtility.HtmlEncode(linkText)}</a>"; return $"<a href=\"/docs/{slug}\">{System.Net.WebUtility.HtmlEncode(linkText)}</a>";
}); });
} }
@@ -221,8 +153,8 @@ public class DocsService
.Replace(".", "") .Replace(".", "")
.Replace("(", "") .Replace("(", "")
.Replace(")", ""); .Replace(")", "");
slug = Regex.Replace(slug, @"[^a-z0-9\-]", ""); slug = System.Text.RegularExpressions.Regex.Replace(slug, @"[^a-z0-9\-]", "");
slug = Regex.Replace(slug, @"-+", "-"); slug = System.Text.RegularExpressions.Regex.Replace(slug, @"-+", "-");
return slug.Trim('-'); return slug.Trim('-');
} }
} }
-365
View File
@@ -1,365 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace Web.Services;
public class RegionState
{
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 int PredatorMeeples { get; set; }
public int PreyMeeples { get; set; }
public int FloraMeeples { get; set; }
public RegionState Clone()
{
return new RegionState
{
Name = Name,
Slug = Slug,
Terrain = Terrain,
X = X,
Y = Y,
Connections = new List<string>(Connections),
PredatorMeeples = PredatorMeeples,
PreyMeeples = PreyMeeples,
FloraMeeples = FloraMeeples
};
}
}
public class TerrainActivation
{
public string MeepleType { get; set; } = "";
public string RegionType { get; set; } = "";
}
public class TurnEvent
{
public int TurnNumber { get; set; }
public List<TerrainActivation> Activations { get; set; } = new();
public List<string> Details { get; set; } = new();
}
public class SimulationData
{
public List<RegionState> Regions { get; set; } = new();
public List<RegionState> InitialRegions { get; set; } = new();
public List<TurnEvent> Events { get; set; } = new();
public List<List<RegionState>> Timeline { get; set; } = new();
public int CurrentTurn { get; set; }
public bool IsInitialized { get; set; }
}
public class GameSimulationService
{
private readonly Random _rng = new();
public SimulationData Data { get; private set; } = new();
public void RunSimulation()
{
Data = new SimulationData();
var regions = CreateRegions();
Data.Regions = regions;
AddInitialFlora();
AddRandomMeepleCombos();
Data.InitialRegions = Data.Regions.Select(r => r.Clone()).ToList();
GenerateAndApplyEvents();
Data.IsInitialized = true;
}
public void RandomizeEvents()
{
RunSimulation();
}
public List<RegionState> GetCurrentState()
{
return Data.Timeline[Data.CurrentTurn].Select(r => r.Clone()).ToList();
}
public TurnEvent? GetCurrentEvent()
{
return Data.CurrentTurn > 0
? Data.Events.ElementAtOrDefault(Data.CurrentTurn - 1)
: null;
}
public List<RegionState> GetPreviousState()
{
var prev = Data.CurrentTurn > 0 ? Data.CurrentTurn - 1 : 0;
return Data.Timeline[prev].Select(r => r.Clone()).ToList();
}
public (int predators, int prey, int flora)[] GetPopulationHistory()
{
return Data.Timeline.Select(t => (
t.Sum(r => r.PredatorMeeples),
t.Sum(r => r.PreyMeeples),
t.Sum(r => r.FloraMeeples)
)).ToArray();
}
private void GenerateAndApplyEvents()
{
Data.Timeline.Add(Data.Regions.Select(r => r.Clone()).ToList());
for (int turn = 1; turn <= 20; turn++)
{
var turnEvent = GenerateTurnEvent(turn);
ApplyTurnEvent(turnEvent);
Data.Events.Add(turnEvent);
Data.Timeline.Add(Data.Regions.Select(r => r.Clone()).ToList());
}
Data.CurrentTurn = 0;
}
private TurnEvent GenerateTurnEvent(int turnNumber)
{
string[] meepleTypes = { "Predator", "Prey", "Flora" };
string[] regionTypes = { "Grass", "Forest", "Mountain", "Water", "Wasteland" };
var numTypes = _rng.Next(1, 4);
var selectedTypes = regionTypes.OrderBy(_ => _rng.Next()).Take(numTypes).ToList();
var activations = selectedTypes.Select(rt => new TerrainActivation
{
RegionType = rt,
MeepleType = meepleTypes[_rng.Next(3)]
}).ToList();
return new TurnEvent
{
TurnNumber = turnNumber,
Activations = activations,
Details = new List<string>()
};
}
private void ApplyTurnEvent(TurnEvent turnEvent)
{
var details = new List<string>();
foreach (var activation in turnEvent.Activations)
{
foreach (var region in Data.Regions.Where(r => r.Terrain == activation.RegionType))
{
switch (activation.MeepleType)
{
case "Flora":
region.FloraMeeples++;
details.Add($"{region.Name}: +1 Flora");
break;
case "Predator":
ApplyPredatorActivation(region, details);
break;
case "Prey":
ApplyPreyActivation(region, details);
break;
}
}
}
turnEvent.Details = details;
}
private void ApplyPredatorActivation(RegionState region, List<string> details)
{
if (region.PredatorMeeples <= 0)
return;
int total = region.PredatorMeeples;
int canEat = Math.Min(total, region.PreyMeeples);
int mustTravel = total - canEat;
if (canEat > 0)
{
region.PreyMeeples -= canEat;
region.PredatorMeeples += canEat;
details.Add($"{region.Name}: {canEat} predators ate prey, +{canEat} new predators");
}
if (mustTravel > 0)
{
var target = FindBestTravelTarget(region, r => r.PreyMeeples);
if (target != null)
{
region.PredatorMeeples -= mustTravel;
target.PredatorMeeples += mustTravel;
details.Add($"{region.Name}: {mustTravel} predators traveled to {target.Name}");
}
}
}
private void ApplyPreyActivation(RegionState region, List<string> details)
{
if (region.PreyMeeples <= 0)
return;
int total = region.PreyMeeples;
int canEat = Math.Min(total, region.FloraMeeples);
int mustTravel = total - canEat;
if (canEat > 0)
{
region.FloraMeeples -= canEat;
region.PreyMeeples += canEat;
details.Add($"{region.Name}: {canEat} prey ate flora, +{canEat} new prey");
}
if (mustTravel > 0)
{
var target = FindBestTravelTarget(region, r => r.FloraMeeples);
if (target != null)
{
region.PreyMeeples -= mustTravel;
target.PreyMeeples += mustTravel;
details.Add($"{region.Name}: {mustTravel} prey traveled to {target.Name}");
}
}
}
private RegionState? FindBestTravelTarget(RegionState region, Func<RegionState, int> resourceCount)
{
var direct = region.Connections
.Select(name => Data.Regions.FirstOrDefault(r => r.Name == name))
.OfType<RegionState>()
.Where(r => resourceCount(r) > 0)
.ToList();
if (direct.Count > 0)
{
return direct.OrderByDescending(r => resourceCount(r)).First();
}
// BFS to find nearest region with resource, return first step along path
var visited = new HashSet<string> { region.Name };
var prev = new Dictionary<string, string>();
var queue = new Queue<string>();
foreach (var conn in region.Connections)
{
if (!visited.Contains(conn))
{
visited.Add(conn);
prev[conn] = region.Name;
queue.Enqueue(conn);
}
}
while (queue.Count > 0)
{
var currentName = queue.Dequeue();
var current = Data.Regions.FirstOrDefault(r => r.Name == currentName);
if (current == null) continue;
if (resourceCount(current) > 0)
{
// Walk back to find the first step from the origin region
var step = currentName;
while (prev[step] != region.Name)
step = prev[step];
return Data.Regions.FirstOrDefault(r => r.Name == step);
}
foreach (var conn in current.Connections)
{
if (!visited.Contains(conn))
{
visited.Add(conn);
prev[conn] = currentName;
queue.Enqueue(conn);
}
}
}
return null;
}
private void AddInitialFlora()
{
var excluded = new HashSet<string>
{
"Grass 1", "Grass 2", "Wasteland 1",
"Mountain 1", "Mountain 2", "Mountain 3", "Mountain 4", "Mountain 5"
};
foreach (var region in Data.Regions)
{
if (!excluded.Contains(region.Name))
{
region.FloraMeeples = 1;
}
}
}
private void AddRandomMeepleCombos()
{
var combos = new List<(int pred, int prey, int flora)>();
for (int total = 1; total <= 3; total++)
{
for (int p = 0; p <= total; p++)
{
for (int r = 0; r <= total - p; r++)
{
int f = total - p - r;
combos.Add((p, r, f));
}
}
}
var excluded = new HashSet<string>
{
"Grass 1", "Grass 2", "Wasteland 1",
"Mountain 1", "Mountain 2", "Mountain 3", "Mountain 4", "Mountain 5"
};
var shuffled = combos.OrderBy(_ => _rng.Next()).ToList();
for (int i = 0; i < Data.Regions.Count; i++)
{
if (!excluded.Contains(Data.Regions[i].Name))
{
var (p, r, f) = shuffled[i % shuffled.Count];
Data.Regions[i].PredatorMeeples = p;
Data.Regions[i].PreyMeeples = r;
Data.Regions[i].FloraMeeples += f;
}
}
}
private static List<RegionState> CreateRegions()
{
return new List<RegionState>
{
new() { Name = "Grass 1", Slug = "grass-1", Terrain = "Grass", X = 15, Y = 260, Connections = new() { "Mountain 1", "Water 2", "Forest 1" } },
new() { Name = "Grass 2", Slug = "grass-2", Terrain = "Grass", X = 150, Y = 400, Connections = new() { "Forest 1", "Forest 2" } },
new() { Name = "Grass 3", Slug = "grass-3", Terrain = "Grass", X = 300, Y = 230, Connections = new() { "Water 2", "Mountain 2", "Mountain 3" } },
new() { Name = "Grass 4", Slug = "grass-4", Terrain = "Grass", X = 510, Y = 30, Connections = new() { "Mountain 4", "Wasteland 1" } },
new() { Name = "Grass 5", Slug = "grass-5", Terrain = "Grass", X = 550, Y = 290, Connections = new() { "Mountain 5", "Water 5", "Wasteland 1", "Forest 4" } },
new() { Name = "Forest 1", Slug = "forest-1", Terrain = "Forest", X = 60, Y = 330, Connections = new() { "Grass 1", "Grass 2", "Water 1", "Forest 2" } },
new() { Name = "Forest 2", Slug = "forest-2", Terrain = "Forest", X = 250, Y = 370, Connections = new() { "Grass 2", "Water 2", "Forest 1" } },
new() { Name = "Forest 3", Slug = "forest-3", Terrain = "Forest", X = 150, Y = 125, Connections = new() { "Water 2", "Water 3" } },
new() { Name = "Forest 4", Slug = "forest-4", Terrain = "Forest", X = 420, Y = 100, Connections = new() { "Grass 4", "Wasteland 1", "Grass 5" } },
new() { Name = "Forest 5", Slug = "forest-5", Terrain = "Forest", X = 470, Y = 410, Connections = new() { "Water 5" } },
new() { Name = "Mountain 1", Slug = "mountain-1", Terrain = "Mountain", X = 20, Y = 120, Connections = new() { "Grass 1", "Water 3", "Forest 3" } },
new() { Name = "Mountain 2", Slug = "mountain-2", Terrain = "Mountain", X = 260, Y = 110, Connections = new() { "Water 3", "Forest 3" } },
new() { Name = "Mountain 3", Slug = "mountain-3", Terrain = "Mountain", X = 380, Y = 180, Connections = new() { "Mountain 5", "Forest 4", "Grass 3" } },
new() { Name = "Mountain 4", Slug = "mountain-4", Terrain = "Mountain", X = 370, Y = 30, Connections = new() { "Forest 4", "Grass 4" } },
new() { Name = "Mountain 5", Slug = "mountain-5", Terrain = "Mountain", X = 430, Y = 330, Connections = new() { "Grass 5", "Mountain 3" } },
new() { Name = "Water 1", Slug = "water-1", Terrain = "Water", X = 30, Y = 410, Connections = new() { "Forest 1" } },
new() { Name = "Water 2", Slug = "water-2", Terrain = "Water", X = 140, Y = 260, Connections = new() { "Forest 1", "Forest 2", "Grass 1", "Forest 3", "Grass 3" } },
new() { Name = "Water 3", Slug = "water-3", Terrain = "Water", X = 50, Y = 50, Connections = new() { "Mountain 1", "Mountain 2", "Forest 3" } },
new() { Name = "Water 4", Slug = "water-4", Terrain = "Water", X = 640, Y = 110, Connections = new() { "Grass 4", "Wasteland 1" } },
new() { Name = "Water 5", Slug = "water-5", Terrain = "Water", X = 600, Y = 400, Connections = new() { "Grass 5", "Forest 5" } },
new() { Name = "Wasteland 1", Slug = "wasteland-1", Terrain = "Wasteland", X = 560, Y = 120, Connections = new() { "Forest 4", "Grass 4", "Grass 5", "Water 4" } }
};
}
}
-1
View File
@@ -11,7 +11,6 @@
<PackageReference Include="Markdig" Version="0.40.0"/> <PackageReference Include="Markdig" Version="0.40.0"/>
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="10.0.9"/> <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="10.0.9"/>
<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="Telerik.UI.for.Blazor" Version="14.0.0" />
</ItemGroup> </ItemGroup>
<Target Name="SyncDocsNotes" BeforeTargets="BeforeBuild"> <Target Name="SyncDocsNotes" BeforeTargets="BeforeBuild">
-2
View File
@@ -9,6 +9,4 @@
@using Web @using Web
@using Web.Layout @using Web.Layout
@using Web.Models @using Web.Models
@using Telerik.Blazor
@using Telerik.Blazor.Components
@using Web.Services @using Web.Services
+49 -640
View File
@@ -1,66 +1,5 @@
:root {
--primary: #3a5a40;
--primary-light: #588157;
--primary-dark: #344e41;
--accent: #a3b18a;
--bg-dark: #1b1c17;
--bg-sidebar: #24251f;
--text-main: #dad7cd;
--text-muted: #a3b18a;
--border-color: rgba(218, 215, 205, 0.1);
/* Telerik UI Overrides - Earthborne Ranger Theme */
--kendo-color-app-surface: var(--bg-dark);
--kendo-color-on-app-surface: var(--text-main);
--kendo-color-subtle: var(--text-muted);
--kendo-color-surface: var(--bg-sidebar);
--kendo-color-surface-alt: rgba(255, 255, 255, 0.02);
--kendo-color-border: var(--border-color);
--kendo-color-border-alt: rgba(255, 255, 255, 0.15);
--kendo-color-base: var(--bg-sidebar);
--kendo-color-base-hover: rgba(255, 255, 255, 0.05);
--kendo-color-base-active: rgba(255, 255, 255, 0.1);
--kendo-color-on-base: var(--text-main);
--kendo-color-primary: var(--primary);
--kendo-color-primary-hover: var(--primary-light);
--kendo-color-primary-active: var(--primary-dark);
--kendo-color-on-primary: #ffffff;
--kendo-color-primary-subtle: rgba(58, 90, 64, 0.15);
--kendo-color-primary-on-subtle: var(--primary-light);
--kendo-color-secondary: var(--accent);
--kendo-color-on-secondary: var(--bg-dark);
--kendo-color-success: var(--primary-light);
--kendo-color-warning: #e9c46a;
--kendo-color-error: #e76f51;
--kendo-color-info: #5fa8d3;
--kendo-color-tertiary: #d4a373;
--kendo-color-inverse: #ffffff;
--kendo-color-on-inverse: #000000;
/* Series Colors */
--kendo-color-series: var(--primary);
--kendo-color-series-a: var(--kendo-color-series);
--kendo-color-series-b: var(--accent);
--kendo-color-series-c: var(--primary-light);
--kendo-color-series-d: #d4a373;
--kendo-color-series-e: #ccd5ae;
--kendo-color-series-f: #e9edc9;
}
html, body { html, body {
font-family: 'Inter', system-ui, -apple-system, sans-serif; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
background-color: var(--bg-dark);
color: var(--text-main);
}
h1, h2, h3, h4, h5, h6 {
font-family: 'Outfit', sans-serif;
font-weight: 600;
color: #fff;
} }
h1:focus { h1:focus {
@@ -68,30 +7,13 @@ h1:focus {
} }
a, .btn-link { a, .btn-link {
color: var(--primary-light); color: #0071c1;
text-decoration: none;
transition: color 0.2s ease;
}
a:hover, .btn-link:hover {
color: var(--accent);
} }
.btn-primary { .btn-primary {
color: #fff; color: #fff;
background-color: var(--primary); background-color: #1b6ec2;
border-color: var(--primary-dark); border-color: #1861ac;
font-weight: 500;
padding: 0.5rem 1.25rem;
border-radius: 6px;
transition: all 0.2s ease;
}
.btn-primary:hover {
background-color: var(--primary-light);
border-color: var(--primary);
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(58, 90, 64, 0.3);
} }
.btn:focus, .btn:active:focus, .btn-link.nav-link:focus, .form-control:focus, .form-check-input:focus { .btn:focus, .btn:active:focus, .btn-link.nav-link:focus, .form-control:focus, .form-check-input:focus {
@@ -196,208 +118,95 @@ code {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-file-text' viewBox='0 0 16 16'%3E%3Cpath d='M5 4a.5.5 0 0 0 0 1h6a.5.5 0 0 0 0-1H5zm-.5 2.5A.5.5 0 0 1 5 6h6a.5.5 0 0 1 0 1H5a.5.5 0 0 1-.5-.5zM5 8a.5.5 0 0 0 0 1h6a.5.5 0 0 0 0-1H5zm0 2a.5.5 0 0 0 0 1h3a.5.5 0 0 0 0-1H5z'/%3E%3Cpath d='M2 2a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V2zm10-1H4a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V2a1 1 0 0 0-1-1z'/%3E%3C/svg%3E"); background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-file-text' viewBox='0 0 16 16'%3E%3Cpath d='M5 4a.5.5 0 0 0 0 1h6a.5.5 0 0 0 0-1H5zm-.5 2.5A.5.5 0 0 1 5 6h6a.5.5 0 0 1 0 1H5a.5.5 0 0 1-.5-.5zM5 8a.5.5 0 0 0 0 1h6a.5.5 0 0 0 0-1H5zm0 2a.5.5 0 0 0 0 1h3a.5.5 0 0 0 0-1H5z'/%3E%3Cpath d='M2 2a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V2zm10-1H4a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V2a1 1 0 0 0-1-1z'/%3E%3C/svg%3E");
} }
.bi-graph-up-nav-menu {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-graph-up' viewBox='0 0 16 16'%3E%3Cpath fill-rule='evenodd' d='M0 0h1v15h15v1H0V0Zm14.817 3.113a.5.5 0 0 1 .07.704l-4.5 5.5a.5.5 0 0 1-.74.037L7.06 6.767l-3.656 5.027a.5.5 0 0 1-.808-.588l4-5.5a.5.5 0 0 1 .758-.06l2.609 2.61 4.15-5.073a.5.5 0 0 1 .704-.07Z'/%3E%3C/svg%3E");
}
.hero-section {
background: linear-gradient(135deg, var(--bg-sidebar) 0%, var(--bg-dark) 100%);
border: 1px solid var(--border-color);
border-radius: 16px;
margin-bottom: 2rem;
position: relative;
overflow: hidden;
}
.hero-section::before {
content: "";
position: absolute;
top: -50%;
left: -50%;
width: 200%;
height: 200%;
background: radial-gradient(circle, rgba(58, 90, 64, 0.1) 0%, transparent 70%);
z-index: 0;
}
.hero-content {
position: relative;
z-index: 1;
}
.lead {
color: var(--text-muted);
font-weight: 400;
}
.docs-grid { .docs-grid {
display: grid; display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 1.5rem; gap: 1rem;
margin-top: 1.5rem; margin-top: 1rem;
} }
.docs-card { .docs-card {
display: flex; display: block;
flex-direction: column; padding: 0.75rem 1rem;
padding: 1.5rem; border: 1px solid #dee2e6;
background: var(--bg-sidebar); border-radius: 8px;
border: 1px solid var(--border-color);
border-radius: 12px;
text-decoration: none; text-decoration: none;
color: var(--text-main); color: inherit;
transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1); transition: box-shadow 0.15s ease-in-out, border-color 0.15s ease-in-out;
height: 100%;
} }
.docs-card:hover { .docs-card:hover {
border-color: var(--primary-light); border-color: #1b6ec2;
transform: translateY(-4px); box-shadow: 0 2px 8px rgba(0,0,0,0.1);
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.3);
text-decoration: none; text-decoration: none;
color: #fff;
} }
.docs-card h3 { .docs-card h3 {
margin: 0.5rem 0 0.75rem 0; margin: 0;
font-size: 1.25rem; font-size: 1rem;
color: #fff; font-weight: 500;
font-family: 'Outfit', sans-serif;
} }
.frontmatter-section { .frontmatter-section {
margin: 2rem 0; margin: 1rem 0;
padding: 1.25rem; padding: 0.5rem;
border: 1px solid var(--border-color); border: 1px solid #e9ecef;
border-radius: 12px; border-radius: 6px;
background: rgba(255, 255, 255, 0.02); background: #f8f9fa;
} }
.frontmatter-section summary { .frontmatter-section summary {
cursor: pointer; cursor: pointer;
font-weight: 600; font-weight: 600;
color: var(--accent); color: #495057;
padding: 0; padding: 0.25rem 0;
font-family: 'Outfit', sans-serif;
} }
table.frontmatter { table.frontmatter {
width: 100%; width: 100%;
border-collapse: separate; border-collapse: collapse;
border-spacing: 0; margin-top: 0.5rem;
margin-top: 1rem;
font-size: 0.9rem; font-size: 0.9rem;
border-radius: 8px;
overflow: hidden;
border: 1px solid var(--border-color);
} }
table.frontmatter td { table.frontmatter td {
padding: 0.75rem 1rem; padding: 0.25rem 0.5rem;
border-bottom: 1px solid var(--border-color); border: 1px solid #dee2e6;
vertical-align: top; vertical-align: top;
background: transparent;
color: var(--text-main);
}
table.frontmatter tr:last-child td {
border-bottom: none;
} }
table.frontmatter td.fm-key { table.frontmatter td.fm-key {
font-weight: 600; font-weight: 600;
color: var(--accent); color: #495057;
white-space: nowrap; white-space: nowrap;
width: 30%; width: 1%;
background: rgba(255, 255, 255, 0.03); background: #e9ecef;
} }
.markdown-body { .markdown-body {
line-height: 1.8; line-height: 1.7;
margin-top: 2rem; margin-top: 1rem;
color: #d1d1d1;
} }
.markdown-body h1 { .markdown-body h1 { font-size: 1.75rem; margin: 1.5rem 0 0.75rem; }
font-size: 2.25rem; .markdown-body h2 { font-size: 1.4rem; margin: 1.25rem 0 0.5rem; }
margin: 2rem 0 1rem; .markdown-body h3 { font-size: 1.15rem; margin: 1rem 0 0.5rem; }
border-bottom: 1px solid var(--border-color); .markdown-body p { margin: 0.5rem 0; }
padding-bottom: 0.5rem; .markdown-body ul, .markdown-body ol { margin: 0.5rem 0; padding-left: 1.5rem; }
} .markdown-body table { border-collapse: collapse; margin: 0.75rem 0; }
.markdown-body th, .markdown-body td { border: 1px solid #dee2e6; padding: 0.4rem 0.6rem; }
.markdown-body h2 { .markdown-body th { background: #f8f9fa; }
font-size: 1.75rem; .markdown-body code { background: #f0f0f0; padding: 0.15rem 0.3rem; border-radius: 3px; font-size: 0.9em; }
margin: 2rem 0 1rem; .markdown-body pre code { background: none; padding: 0; }
color: var(--accent); .markdown-body blockquote { border-left: 3px solid #dee2e6; padding-left: 1rem; color: #6c757d; margin: 0.75rem 0; }
}
.markdown-body h3 {
font-size: 1.4rem;
margin: 1.5rem 0 0.75rem;
color: var(--primary-light);
}
.markdown-body p {
margin: 1rem 0;
}
.markdown-body table {
width: 100%;
max-width: 100%;
display: block;
overflow-x: auto;
overflow-y: hidden;
border-collapse: separate;
border-spacing: 0;
margin: 1.5rem 0;
border-radius: 12px;
border: 1px solid var(--border-color);
}
.markdown-body th, .markdown-body td {
padding: 0.75rem 1rem;
border-bottom: 1px solid var(--border-color);
}
.markdown-body th {
background: rgba(255, 255, 255, 0.05);
color: var(--accent);
font-weight: 600;
text-align: left;
}
.markdown-body tr:last-child td {
border-bottom: none;
}
.markdown-body code {
background: rgba(255, 255, 255, 0.1);
color: var(--accent);
padding: 0.2rem 0.4rem;
border-radius: 4px;
font-size: 0.9em;
}
.markdown-body pre code {
background: none;
padding: 0;
}
.markdown-body blockquote {
border-left: 3px solid #dee2e6;
padding-left: 1rem;
color: #6c757d;
margin: 0.75rem 0;
}
.map-container { .map-container {
max-width: 100%; max-width: 100%;
margin: 1rem 0; margin: 1.5rem 0;
border: 1px solid var(--border-color); border: 1px solid #dee2e6;
border-radius: 12px; border-radius: 8px;
overflow: hidden; overflow: hidden;
background: #0f100d; background: #1a1a2e;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.5);
} }
.map-svg { .map-svg {
@@ -420,403 +229,3 @@ table.frontmatter td.fm-key {
color: #1b6ec2; color: #1b6ec2;
border: 1px solid #b8daff; border: 1px solid #b8daff;
} }
.embed {
margin: 1rem 0;
padding: 0.75rem 1rem;
border-left: 3px solid rgba(255, 255, 255, 0.2);
background: rgba(255, 255, 255, 0.03);
border-radius: 0 6px 6px 0;
}
.embed-error {
color: #e06c75;
font-style: italic;
padding: 0.5rem 0;
}
/* Document Content */
.doc-content {
max-width: 900px;
margin: 0 auto;
background: var(--bg-sidebar);
padding: 2.5rem;
border-radius: 16px;
border: 1px solid var(--border-color);
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.2);
}
/* Gear Styles */
.gear-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
gap: 1.5rem;
}
.gear-card {
background: var(--bg-sidebar);
border: 1px solid var(--border-color);
border-radius: 12px;
overflow: hidden;
transition: all 0.2s ease;
}
.gear-card:hover {
border-color: var(--primary-light);
transform: translateY(-2px);
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.3);
}
.gear-header {
background: rgba(255, 255, 255, 0.03);
padding: 1.25rem;
border-bottom: 1px solid var(--border-color);
}
.gear-body {
padding: 1.25rem;
}
.gear-stat {
display: flex;
justify-content: space-between;
margin-bottom: 0.5rem;
font-size: 0.9rem;
}
.gear-stat-label {
color: var(--text-muted);
}
.gear-stat-value {
color: var(--text-main);
font-weight: 500;
}
/* Telerik Grid Overrides */
.k-grid {
background-color: var(--kendo-color-surface) !important;
border: 1px solid var(--kendo-color-border) !important;
color: var(--kendo-color-on-surface) !important;
border-radius: 12px !important;
overflow: hidden !important;
}
.k-grid-header,
.k-grid-header-wrap,
.k-header,
.k-table-header,
.k-table-thead,
.k-grid-header-table,
.k-table-header-wrap,
.k-table-thead > tr,
.k-column-title,
.k-filter-row {
background-color: var(--kendo-color-app-surface) !important;
background-image: none !important;
border-color: var(--kendo-color-border) !important;
}
.k-grid-header .k-header,
.k-table-th {
background: transparent !important;
color: var(--accent) !important;
border-color: var(--kendo-color-border) !important;
font-family: 'Outfit', sans-serif !important;
font-weight: 600 !important;
text-transform: uppercase;
font-size: 0.85rem;
padding: 1rem !important;
letter-spacing: 0.025em;
}
.k-grid td,
.k-table-td {
padding: 1rem !important;
border-color: var(--kendo-color-border) !important;
background: transparent !important;
vertical-align: middle !important;
}
.k-grid tr,
.k-table-row {
background-color: transparent !important;
}
.k-grid tr.k-alt,
.k-table-row.k-alt,
.k-table-row.k-table-alt-row {
background-color: rgba(255, 255, 255, 0.01) !important;
}
.k-grid tr:hover,
.k-table-row:hover {
background-color: rgba(255, 255, 255, 0.03) !important;
}
.k-grid-content,
.k-grid-table,
.k-table {
background-color: transparent !important;
}
.k-grid-header-wrap {
border-color: var(--border-color) !important;
}
/* Pager Styles */
.k-pager-wrap,
.k-pager,
.k-grid-pager,
.k-pager-td,
.k-pager-numbers-wrap {
background-color: var(--kendo-color-app-surface) !important;
background-image: none !important;
border-top: 1px solid var(--kendo-color-border) !important;
color: var(--text-muted) !important;
padding: 0.75rem !important;
}
.k-pager-numbers .k-link {
background-color: transparent !important;
color: var(--text-muted) !important;
border-radius: 6px !important;
transition: all 0.2s !important;
margin: 0 2px !important;
}
.k-pager-numbers .k-link.k-selected,
.k-pager-numbers .k-link.k-state-selected {
background-color: var(--kendo-color-primary) !important;
color: var(--kendo-color-on-primary) !important;
border-color: var(--primary-light) !important;
}
.k-pager-numbers .k-link:hover {
background-color: var(--kendo-color-primary-hover) !important;
color: var(--kendo-color-on-primary) !important;
}
.k-pager-nav.k-link {
background-color: transparent !important;
color: var(--kendo-color-on-app-surface) !important;
}
/* Filter Row Styles */
.k-filter-row td,
.k-filter-row th {
background-color: rgba(0, 0, 0, 0.2) !important;
padding: 0.5rem 1rem !important;
border-color: var(--kendo-color-border) !important;
}
.k-filtercell .k-textbox,
.k-filtercell .k-input,
.k-filtercell .k-input-inner {
background-color: var(--kendo-color-app-surface) !important;
color: var(--kendo-color-on-app-surface) !important;
border: 1px solid var(--kendo-color-border) !important;
border-radius: 4px !important;
}
.k-filtercell .k-input-inner {
padding: 4px 8px !important;
}
.k-filtercell .k-button {
background-color: var(--kendo-color-primary-active) !important;
border-color: var(--kendo-color-border) !important;
color: var(--kendo-color-on-primary) !important;
}
.k-filtercell .k-button:hover {
background-color: var(--kendo-color-primary) !important;
}
/* Custom scrollbar for grid content */
.k-grid-content::-webkit-scrollbar {
width: 10px;
}
.k-grid-content::-webkit-scrollbar-track {
background: var(--kendo-color-app-surface);
}
.k-grid-content::-webkit-scrollbar-thumb {
background: var(--kendo-color-primary-active);
border-radius: 5px;
border: 2px solid var(--kendo-color-app-surface);
}
.k-grid-content::-webkit-scrollbar-thumb:hover {
background: var(--kendo-color-primary-hover);
}
/* Typography in Grid */
.k-grid td .fw-bold {
color: var(--primary-light);
}
.k-grid td .fw-bold:hover {
color: var(--accent);
text-decoration: underline;
}
/* Loading and Empty State */
.k-loading-mask {
background-color: rgba(0, 0, 0, 0.5) !important;
}
.k-loading-image {
background-image: none !important;
}
.k-loading-color {
background-color: var(--primary) !important;
}
.k-grid-norecords {
color: var(--text-muted) !important;
padding: 3rem !important;
text-align: center !important;
}
/* Simulation Page */
.simulation-layout {
max-width: 900px;
margin: 0 auto;
}
.sim-controls {
background: var(--bg-sidebar);
border: 1px solid var(--border-color);
border-radius: 12px;
padding: 0.75rem 1.25rem;
}
.turn-label {
font-size: 1.1rem;
color: var(--text-main);
min-width: 100px;
text-align: center;
}
.btn-outline-light {
color: var(--text-main);
border-color: var(--border-color);
}
.btn-outline-light:hover {
background: var(--primary);
border-color: var(--primary);
color: #fff;
}
.btn-outline-light:disabled {
opacity: 0.3;
}
.event-badge {
font-size: 0.85rem;
padding: 0.35em 0.75em;
}
.sim-map-container {
border: 1px solid var(--border-color);
border-radius: 12px;
overflow: hidden;
background: #0f100d;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.5);
}
.sim-svg {
width: 100%;
height: auto;
display: block;
}
.sim-region {
transition: opacity 0.2s;
}
.sim-active circle:last-child {
animation: pulse-glow 1.5s ease-in-out infinite;
}
@keyframes pulse-glow {
0%, 100% { stroke-opacity: 1; }
50% { stroke-opacity: 0.4; }
}
.sim-details {
background: var(--bg-sidebar);
border: 1px solid var(--border-color);
border-radius: 12px;
padding: 1rem 1.25rem;
max-height: 180px;
}
.sim-details-scroll {
display: flex;
flex-direction: column;
gap: 0.25rem;
overflow-y: auto;
max-height: 120px;
}
.sim-detail-item {
font-size: 0.85rem;
color: var(--text-muted);
padding: 0.2rem 0;
border-bottom: 1px solid rgba(255,255,255,0.04);
}
.sim-detail-item:last-child {
border-bottom: none;
}
.sim-totals {
font-size: 0.9rem;
color: var(--text-main);
}
.total-dot {
display: inline-block;
width: 10px;
height: 10px;
border-radius: 50%;
margin-right: 6px;
vertical-align: middle;
}
.bg-danger {
background-color: #e76f51 !important;
}
.bg-warning {
background-color: #e9c46a !important;
color: #000 !important;
}
.bg-success {
background-color: #4caf50 !important;
}
/* Simulation Chart */
.sim-chart {
background: var(--bg-sidebar);
border: 1px solid var(--border-color);
border-radius: 12px;
padding: 1rem 1.25rem;
}
.sim-chart-svg {
width: 100%;
height: auto;
display: block;
}
.sim-chart-legend {
font-size: 0.85rem;
color: var(--text-muted);
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 589 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 904 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 294 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 590 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 589 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 380 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 404 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 404 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 404 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 334 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 444 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 404 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 MiB

+107 -95
View File
@@ -1,5 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 700 461"> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 760 530">
<image href="Map.png" width="700" height="461" preserveAspectRatio="xMidYMid meet"/> <rect width="100%" height="100%" fill="#1a1a2e" rx="8"/>
<defs> <defs>
<radialGradient id="glow-Grass" cx="50%" cy="50%" r="50%"> <radialGradient id="glow-Grass" cx="50%" cy="50%" r="50%">
<stop offset="0%" stop-color="#4caf50" stop-opacity="0.3"/> <stop offset="0%" stop-color="#4caf50" stop-opacity="0.3"/>
@@ -22,164 +22,176 @@
<stop offset="100%" stop-color="#8d6e63" stop-opacity="0"/> <stop offset="100%" stop-color="#8d6e63" stop-opacity="0"/>
</radialGradient> </radialGradient>
</defs> </defs>
<line x1="60" y1="330" x2="15" y2="260" stroke="rgba(255,255,255,0.12)" stroke-width="2" stroke-linecap="round"/> <line x1="120" y1="390" x2="75" y2="320" stroke="rgba(255,255,255,0.12)" stroke-width="2" stroke-linecap="round"/>
<line x1="60" y1="330" x2="150" y2="400" stroke="rgba(255,255,255,0.12)" stroke-width="2" stroke-linecap="round"/> <line x1="120" y1="390" x2="210" y2="460" stroke="rgba(255,255,255,0.12)" stroke-width="2" stroke-linecap="round"/>
<line x1="60" y1="330" x2="30" y2="410" stroke="rgba(255,255,255,0.12)" stroke-width="2" stroke-linecap="round"/> <line x1="120" y1="390" x2="90" y2="470" stroke="rgba(255,255,255,0.12)" stroke-width="2" stroke-linecap="round"/>
<line x1="60" y1="330" x2="250" y2="370" stroke="rgba(255,255,255,0.12)" stroke-width="2" stroke-linecap="round"/> <line x1="120" y1="390" x2="310" y2="430" stroke="rgba(255,255,255,0.12)" stroke-width="2" stroke-linecap="round"/>
<line x1="250" y1="370" x2="150" y2="400" stroke="rgba(255,255,255,0.12)" stroke-width="2" stroke-linecap="round"/> <line x1="310" y1="430" x2="210" y2="460" stroke="rgba(255,255,255,0.12)" stroke-width="2" stroke-linecap="round"/>
<line x1="250" y1="370" x2="140" y2="260" stroke="rgba(255,255,255,0.12)" stroke-width="2" stroke-linecap="round"/> <line x1="310" y1="430" x2="200" y2="320" stroke="rgba(255,255,255,0.12)" stroke-width="2" stroke-linecap="round"/>
<line x1="150" y1="125" x2="140" y2="260" stroke="rgba(255,255,255,0.12)" stroke-width="2" stroke-linecap="round"/> <line x1="210" y1="185" x2="200" y2="320" stroke="rgba(255,255,255,0.12)" stroke-width="2" stroke-linecap="round"/>
<line x1="150" y1="125" x2="50" y2="50" stroke="rgba(255,255,255,0.12)" stroke-width="2" stroke-linecap="round"/> <line x1="210" y1="185" x2="110" y2="110" stroke="rgba(255,255,255,0.12)" stroke-width="2" stroke-linecap="round"/>
<line x1="420" y1="100" x2="510" y2="30" stroke="rgba(255,255,255,0.12)" stroke-width="2" stroke-linecap="round"/> <line x1="480" y1="160" x2="570" y2="90" stroke="rgba(255,255,255,0.12)" stroke-width="2" stroke-linecap="round"/>
<line x1="420" y1="100" x2="560" y2="120" stroke="rgba(255,255,255,0.12)" stroke-width="2" stroke-linecap="round"/> <line x1="480" y1="160" x2="620" y2="180" stroke="rgba(255,255,255,0.12)" stroke-width="2" stroke-linecap="round"/>
<line x1="420" y1="100" x2="550" y2="290" stroke="rgba(255,255,255,0.12)" stroke-width="2" stroke-linecap="round"/> <line x1="480" y1="160" x2="610" y2="350" stroke="rgba(255,255,255,0.12)" stroke-width="2" stroke-linecap="round"/>
<line x1="470" y1="410" x2="600" y2="400" stroke="rgba(255,255,255,0.12)" stroke-width="2" stroke-linecap="round"/> <line x1="530" y1="470" x2="660" y2="460" stroke="rgba(255,255,255,0.12)" stroke-width="2" stroke-linecap="round"/>
<line x1="15" y1="260" x2="20" y2="120" stroke="rgba(255,255,255,0.12)" stroke-width="2" stroke-linecap="round"/> <line x1="75" y1="320" x2="80" y2="180" stroke="rgba(255,255,255,0.12)" stroke-width="2" stroke-linecap="round"/>
<line x1="15" y1="260" x2="140" y2="260" stroke="rgba(255,255,255,0.12)" stroke-width="2" stroke-linecap="round"/> <line x1="75" y1="320" x2="200" y2="320" stroke="rgba(255,255,255,0.12)" stroke-width="2" stroke-linecap="round"/>
<line x1="300" y1="230" x2="140" y2="260" stroke="rgba(255,255,255,0.12)" stroke-width="2" stroke-linecap="round"/> <line x1="360" y1="290" x2="200" y2="320" stroke="rgba(255,255,255,0.12)" stroke-width="2" stroke-linecap="round"/>
<line x1="300" y1="230" x2="260" y2="110" stroke="rgba(255,255,255,0.12)" stroke-width="2" stroke-linecap="round"/> <line x1="360" y1="290" x2="320" y2="170" stroke="rgba(255,255,255,0.12)" stroke-width="2" stroke-linecap="round"/>
<line x1="300" y1="230" x2="380" y2="180" stroke="rgba(255,255,255,0.12)" stroke-width="2" stroke-linecap="round"/> <line x1="360" y1="290" x2="440" y2="240" stroke="rgba(255,255,255,0.12)" stroke-width="2" stroke-linecap="round"/>
<line x1="510" y1="30" x2="370" y2="30" stroke="rgba(255,255,255,0.12)" stroke-width="2" stroke-linecap="round"/> <line x1="570" y1="90" x2="430" y2="90" stroke="rgba(255,255,255,0.12)" stroke-width="2" stroke-linecap="round"/>
<line x1="510" y1="30" x2="560" y2="120" stroke="rgba(255,255,255,0.12)" stroke-width="2" stroke-linecap="round"/> <line x1="570" y1="90" x2="620" y2="180" stroke="rgba(255,255,255,0.12)" stroke-width="2" stroke-linecap="round"/>
<line x1="550" y1="290" x2="430" y2="330" stroke="rgba(255,255,255,0.12)" stroke-width="2" stroke-linecap="round"/> <line x1="610" y1="350" x2="490" y2="390" stroke="rgba(255,255,255,0.12)" stroke-width="2" stroke-linecap="round"/>
<line x1="550" y1="290" x2="600" y2="400" stroke="rgba(255,255,255,0.12)" stroke-width="2" stroke-linecap="round"/> <line x1="610" y1="350" x2="660" y2="460" stroke="rgba(255,255,255,0.12)" stroke-width="2" stroke-linecap="round"/>
<line x1="550" y1="290" x2="560" y2="120" stroke="rgba(255,255,255,0.12)" stroke-width="2" stroke-linecap="round"/> <line x1="610" y1="350" x2="620" y2="180" stroke="rgba(255,255,255,0.12)" stroke-width="2" stroke-linecap="round"/>
<line x1="20" y1="120" x2="50" y2="50" stroke="rgba(255,255,255,0.12)" stroke-width="2" stroke-linecap="round"/> <line x1="80" y1="180" x2="110" y2="110" stroke="rgba(255,255,255,0.12)" stroke-width="2" stroke-linecap="round"/>
<line x1="260" y1="110" x2="50" y2="50" stroke="rgba(255,255,255,0.12)" stroke-width="2" stroke-linecap="round"/> <line x1="320" y1="170" x2="110" y2="110" stroke="rgba(255,255,255,0.12)" stroke-width="2" stroke-linecap="round"/>
<line x1="380" y1="180" x2="430" y2="330" stroke="rgba(255,255,255,0.12)" stroke-width="2" stroke-linecap="round"/> <line x1="440" y1="240" x2="490" y2="390" stroke="rgba(255,255,255,0.12)" stroke-width="2" stroke-linecap="round"/>
<line x1="560" y1="120" x2="640" y2="110" stroke="rgba(255,255,255,0.12)" stroke-width="2" stroke-linecap="round"/> <line x1="620" y1="180" x2="700" y2="170" stroke="rgba(255,255,255,0.12)" stroke-width="2" stroke-linecap="round"/>
<a href="/docs/forest-1" target="_top"> <a href="/docs/forest-1" target="_top">
<circle cx="60" cy="330" r="32" fill="url(#glow-Forest)"/> <circle cx="120" cy="390" r="32" fill="url(#glow-Forest)"/>
<circle cx="60" cy="330" r="18" fill="#2e7d32cc" stroke="rgba(255,255,255,0.5)" stroke-width="2"/> <circle cx="120" cy="390" r="18" fill="#2e7d32" stroke="rgba(255,255,255,0.4)" stroke-width="2"/>
<text x="84" y="334" fill="rgba(255,255,255,0.9)" font-family="system-ui,sans-serif" font-size="11" font-weight="600"> <text x="144" y="394" fill="rgba(255,255,255,0.9)" font-family="system-ui,sans-serif" font-size="11" font-weight="600">
Forest 1</text> Forest 1</text>
</a> </a>
<a href="/docs/forest-2" target="_top"> <a href="/docs/forest-2" target="_top">
<circle cx="250" cy="370" r="32" fill="url(#glow-Forest)"/> <circle cx="310" cy="430" r="32" fill="url(#glow-Forest)"/>
<circle cx="250" cy="370" r="18" fill="#2e7d32cc" stroke="rgba(255,255,255,0.5)" stroke-width="2"/> <circle cx="310" cy="430" r="18" fill="#2e7d32" stroke="rgba(255,255,255,0.4)" stroke-width="2"/>
<text x="274" y="374" fill="rgba(255,255,255,0.9)" font-family="system-ui,sans-serif" font-size="11" font-weight="600"> <text x="334" y="434" fill="rgba(255,255,255,0.9)" font-family="system-ui,sans-serif" font-size="11" font-weight="600">
Forest 2</text> Forest 2</text>
</a> </a>
<a href="/docs/forest-3" target="_top"> <a href="/docs/forest-3" target="_top">
<circle cx="150" cy="125" r="32" fill="url(#glow-Forest)"/> <circle cx="210" cy="185" r="32" fill="url(#glow-Forest)"/>
<circle cx="150" cy="125" r="18" fill="#2e7d32cc" stroke="rgba(255,255,255,0.5)" stroke-width="2"/> <circle cx="210" cy="185" r="18" fill="#2e7d32" stroke="rgba(255,255,255,0.4)" stroke-width="2"/>
<text x="174" y="129" fill="rgba(255,255,255,0.9)" font-family="system-ui,sans-serif" font-size="11" font-weight="600"> <text x="234" y="189" fill="rgba(255,255,255,0.9)" font-family="system-ui,sans-serif" font-size="11" font-weight="600">
Forest 3</text> Forest 3</text>
</a> </a>
<a href="/docs/forest-4" target="_top"> <a href="/docs/forest-4" target="_top">
<circle cx="420" cy="100" r="32" fill="url(#glow-Forest)"/> <circle cx="480" cy="160" r="32" fill="url(#glow-Forest)"/>
<circle cx="420" cy="100" r="18" fill="#2e7d32cc" stroke="rgba(255,255,255,0.5)" stroke-width="2"/> <circle cx="480" cy="160" r="18" fill="#2e7d32" stroke="rgba(255,255,255,0.4)" stroke-width="2"/>
<text x="444" y="104" fill="rgba(255,255,255,0.9)" font-family="system-ui,sans-serif" font-size="11" font-weight="600"> <text x="504" y="164" fill="rgba(255,255,255,0.9)" font-family="system-ui,sans-serif" font-size="11" font-weight="600">
Forest 4</text> Forest 4</text>
</a> </a>
<a href="/docs/forest-5" target="_top"> <a href="/docs/forest-5" target="_top">
<circle cx="470" cy="410" r="32" fill="url(#glow-Forest)"/> <circle cx="530" cy="470" r="32" fill="url(#glow-Forest)"/>
<circle cx="470" cy="410" r="18" fill="#2e7d32cc" stroke="rgba(255,255,255,0.5)" stroke-width="2"/> <circle cx="530" cy="470" r="18" fill="#2e7d32" stroke="rgba(255,255,255,0.4)" stroke-width="2"/>
<text x="494" y="414" fill="rgba(255,255,255,0.9)" font-family="system-ui,sans-serif" font-size="11" font-weight="600"> <text x="554" y="474" fill="rgba(255,255,255,0.9)" font-family="system-ui,sans-serif" font-size="11" font-weight="600">
Forest 5</text> Forest 5</text>
</a> </a>
<a href="/docs/grass-1" target="_top"> <a href="/docs/grass-1" target="_top">
<circle cx="15" cy="260" r="32" fill="url(#glow-Grass)"/> <circle cx="75" cy="320" r="32" fill="url(#glow-Grass)"/>
<circle cx="15" cy="260" r="18" fill="#4caf50cc" stroke="rgba(255,255,255,0.5)" stroke-width="2"/> <circle cx="75" cy="320" r="18" fill="#4caf50" stroke="rgba(255,255,255,0.4)" stroke-width="2"/>
<text x="39" y="264" fill="rgba(255,255,255,0.9)" font-family="system-ui,sans-serif" font-size="11" font-weight="600"> <text x="99" y="324" fill="rgba(255,255,255,0.9)" font-family="system-ui,sans-serif" font-size="11" font-weight="600">
Grass 1</text> Grass 1</text>
<text x="39" y="278" fill="rgba(255,255,255,0.45)" font-family="system-ui,sans-serif" font-size="9" font-style="italic"> <text x="99" y="338" fill="rgba(255,255,255,0.45)" font-family="system-ui,sans-serif" font-size="9" font-style="italic">
&#x2605; Lone Tree Station</text> &#x2605; Lone Tree Station</text>
</a> </a>
<a href="/docs/grass-2" target="_top"> <a href="/docs/grass-2" target="_top">
<circle cx="150" cy="400" r="32" fill="url(#glow-Grass)"/> <circle cx="210" cy="460" r="32" fill="url(#glow-Grass)"/>
<circle cx="150" cy="400" r="18" fill="#4caf50cc" stroke="rgba(255,255,255,0.5)" stroke-width="2"/> <circle cx="210" cy="460" r="18" fill="#4caf50" stroke="rgba(255,255,255,0.4)" stroke-width="2"/>
<text x="174" y="404" fill="rgba(255,255,255,0.9)" font-family="system-ui,sans-serif" font-size="11" font-weight="600"> <text x="234" y="464" fill="rgba(255,255,255,0.9)" font-family="system-ui,sans-serif" font-size="11" font-weight="600">
Grass 2</text> Grass 2</text>
<text x="174" y="418" fill="rgba(255,255,255,0.45)" font-family="system-ui,sans-serif" font-size="9" font-style="italic"> <text x="234" y="478" fill="rgba(255,255,255,0.45)" font-family="system-ui,sans-serif" font-size="9" font-style="italic">
&#x2605; Spire</text> &#x2605; Spire</text>
</a> </a>
<a href="/docs/grass-3" target="_top"> <a href="/docs/grass-3" target="_top">
<circle cx="300" cy="230" r="32" fill="url(#glow-Grass)"/> <circle cx="360" cy="290" r="32" fill="url(#glow-Grass)"/>
<circle cx="300" cy="230" r="18" fill="#4caf50cc" stroke="rgba(255,255,255,0.5)" stroke-width="2"/> <circle cx="360" cy="290" r="18" fill="#4caf50" stroke="rgba(255,255,255,0.4)" stroke-width="2"/>
<text x="324" y="234" fill="rgba(255,255,255,0.9)" font-family="system-ui,sans-serif" font-size="11" font-weight="600"> <text x="384" y="294" fill="rgba(255,255,255,0.9)" font-family="system-ui,sans-serif" font-size="11" font-weight="600">
Grass 3</text> Grass 3</text>
</a> </a>
<a href="/docs/grass-4" target="_top"> <a href="/docs/grass-4" target="_top">
<circle cx="510" cy="30" r="32" fill="url(#glow-Grass)"/> <circle cx="570" cy="90" r="32" fill="url(#glow-Grass)"/>
<circle cx="510" cy="30" r="18" fill="#4caf50cc" stroke="rgba(255,255,255,0.5)" stroke-width="2"/> <circle cx="570" cy="90" r="18" fill="#4caf50" stroke="rgba(255,255,255,0.4)" stroke-width="2"/>
<text x="534" y="34" fill="rgba(255,255,255,0.9)" font-family="system-ui,sans-serif" font-size="11" font-weight="600"> <text x="594" y="94" fill="rgba(255,255,255,0.9)" font-family="system-ui,sans-serif" font-size="11" font-weight="600">
Grass 4</text> Grass 4</text>
</a> </a>
<a href="/docs/grass-5" target="_top"> <a href="/docs/grass-5" target="_top">
<circle cx="550" cy="290" r="32" fill="url(#glow-Grass)"/> <circle cx="610" cy="350" r="32" fill="url(#glow-Grass)"/>
<circle cx="550" cy="290" r="18" fill="#4caf50cc" stroke="rgba(255,255,255,0.5)" stroke-width="2"/> <circle cx="610" cy="350" r="18" fill="#4caf50" stroke="rgba(255,255,255,0.4)" stroke-width="2"/>
<text x="574" y="294" fill="rgba(255,255,255,0.9)" font-family="system-ui,sans-serif" font-size="11" font-weight="600"> <text x="634" y="354" fill="rgba(255,255,255,0.9)" font-family="system-ui,sans-serif" font-size="11" font-weight="600">
Grass 5</text> Grass 5</text>
</a> </a>
<a href="/docs/mountain-1" target="_top"> <a href="/docs/mountain-1" target="_top">
<circle cx="20" cy="120" r="32" fill="url(#glow-Mountain)"/> <circle cx="80" cy="180" r="32" fill="url(#glow-Mountain)"/>
<circle cx="20" cy="120" r="18" fill="#78909ccc" stroke="rgba(255,255,255,0.5)" stroke-width="2"/> <circle cx="80" cy="180" r="18" fill="#78909c" stroke="rgba(255,255,255,0.4)" stroke-width="2"/>
<text x="44" y="124" fill="rgba(255,255,255,0.9)" font-family="system-ui,sans-serif" font-size="11" font-weight="600"> <text x="104" y="184" fill="rgba(255,255,255,0.9)" font-family="system-ui,sans-serif" font-size="11" font-weight="600">
Mountain 1</text> Mountain 1</text>
</a> </a>
<a href="/docs/mountain-2" target="_top"> <a href="/docs/mountain-2" target="_top">
<circle cx="260" cy="110" r="32" fill="url(#glow-Mountain)"/> <circle cx="320" cy="170" r="32" fill="url(#glow-Mountain)"/>
<circle cx="260" cy="110" r="18" fill="#78909ccc" stroke="rgba(255,255,255,0.5)" stroke-width="2"/> <circle cx="320" cy="170" r="18" fill="#78909c" stroke="rgba(255,255,255,0.4)" stroke-width="2"/>
<text x="284" y="114" fill="rgba(255,255,255,0.9)" font-family="system-ui,sans-serif" font-size="11" font-weight="600"> <text x="344" y="174" fill="rgba(255,255,255,0.9)" font-family="system-ui,sans-serif" font-size="11" font-weight="600">
Mountain 2</text> Mountain 2</text>
</a> </a>
<a href="/docs/mountain-3" target="_top"> <a href="/docs/mountain-3" target="_top">
<circle cx="380" cy="180" r="32" fill="url(#glow-Mountain)"/> <circle cx="440" cy="240" r="32" fill="url(#glow-Mountain)"/>
<circle cx="380" cy="180" r="18" fill="#78909ccc" stroke="rgba(255,255,255,0.5)" stroke-width="2"/> <circle cx="440" cy="240" r="18" fill="#78909c" stroke="rgba(255,255,255,0.4)" stroke-width="2"/>
<text x="404" y="184" fill="rgba(255,255,255,0.9)" font-family="system-ui,sans-serif" font-size="11" font-weight="600"> <text x="464" y="244" fill="rgba(255,255,255,0.9)" font-family="system-ui,sans-serif" font-size="11" font-weight="600">
Mountain 3</text> Mountain 3</text>
</a> </a>
<a href="/docs/mountain-4" target="_top"> <a href="/docs/mountain-4" target="_top">
<circle cx="370" cy="30" r="32" fill="url(#glow-Mountain)"/> <circle cx="430" cy="90" r="32" fill="url(#glow-Mountain)"/>
<circle cx="370" cy="30" r="18" fill="#78909ccc" stroke="rgba(255,255,255,0.5)" stroke-width="2"/> <circle cx="430" cy="90" r="18" fill="#78909c" stroke="rgba(255,255,255,0.4)" stroke-width="2"/>
<text x="394" y="34" fill="rgba(255,255,255,0.9)" font-family="system-ui,sans-serif" font-size="11" font-weight="600"> <text x="454" y="94" fill="rgba(255,255,255,0.9)" font-family="system-ui,sans-serif" font-size="11" font-weight="600">
Mountain 4</text> Mountain 4</text>
</a> </a>
<a href="/docs/mountain-5" target="_top"> <a href="/docs/mountain-5" target="_top">
<circle cx="430" cy="330" r="32" fill="url(#glow-Mountain)"/> <circle cx="490" cy="390" r="32" fill="url(#glow-Mountain)"/>
<circle cx="430" cy="330" r="18" fill="#78909ccc" stroke="rgba(255,255,255,0.5)" stroke-width="2"/> <circle cx="490" cy="390" r="18" fill="#78909c" stroke="rgba(255,255,255,0.4)" stroke-width="2"/>
<text x="454" y="334" fill="rgba(255,255,255,0.9)" font-family="system-ui,sans-serif" font-size="11" font-weight="600"> <text x="514" y="394" fill="rgba(255,255,255,0.9)" font-family="system-ui,sans-serif" font-size="11" font-weight="600">
Mountain 5</text> Mountain 5</text>
</a> </a>
<a href="/docs/wasteland-1" target="_top"> <a href="/docs/wasteland-1" target="_top">
<circle cx="560" cy="120" r="32" fill="url(#glow-Wasteland)"/> <circle cx="620" cy="180" r="32" fill="url(#glow-Wasteland)"/>
<circle cx="560" cy="120" r="18" fill="#8d6e63cc" stroke="rgba(255,255,255,0.5)" stroke-width="2"/> <circle cx="620" cy="180" r="18" fill="#8d6e63" stroke="rgba(255,255,255,0.4)" stroke-width="2"/>
<text x="584" y="124" fill="rgba(255,255,255,0.9)" font-family="system-ui,sans-serif" font-size="11" font-weight="600"> <text x="644" y="184" fill="rgba(255,255,255,0.9)" font-family="system-ui,sans-serif" font-size="11" font-weight="600">
Wasteland 1</text> Wasteland 1</text>
</a> </a>
<a href="/docs/water-1" target="_top"> <a href="/docs/water-1" target="_top">
<circle cx="30" cy="410" r="32" fill="url(#glow-Water)"/> <circle cx="90" cy="470" r="32" fill="url(#glow-Water)"/>
<circle cx="30" cy="410" r="18" fill="#42a5f5cc" stroke="rgba(255,255,255,0.5)" stroke-width="2"/> <circle cx="90" cy="470" r="18" fill="#42a5f5" stroke="rgba(255,255,255,0.4)" stroke-width="2"/>
<text x="54" y="414" fill="rgba(255,255,255,0.9)" font-family="system-ui,sans-serif" font-size="11" font-weight="600"> <text x="114" y="474" fill="rgba(255,255,255,0.9)" font-family="system-ui,sans-serif" font-size="11" font-weight="600">
Water 1</text> Water 1</text>
<text x="54" y="428" fill="rgba(255,255,255,0.45)" font-family="system-ui,sans-serif" font-size="9" font-style="italic"> <text x="114" y="488" fill="rgba(255,255,255,0.45)" font-family="system-ui,sans-serif" font-size="9" font-style="italic">
&#x2605; Research Station</text> &#x2605; Research Station</text>
</a> </a>
<a href="/docs/water-2" target="_top"> <a href="/docs/water-2" target="_top">
<circle cx="140" cy="260" r="32" fill="url(#glow-Water)"/> <circle cx="200" cy="320" r="32" fill="url(#glow-Water)"/>
<circle cx="140" cy="260" r="18" fill="#42a5f5cc" stroke="rgba(255,255,255,0.5)" stroke-width="2"/> <circle cx="200" cy="320" r="18" fill="#42a5f5" stroke="rgba(255,255,255,0.4)" stroke-width="2"/>
<text x="164" y="264" fill="rgba(255,255,255,0.9)" font-family="system-ui,sans-serif" font-size="11" font-weight="600"> <text x="224" y="324" fill="rgba(255,255,255,0.9)" font-family="system-ui,sans-serif" font-size="11" font-weight="600">
Water 2</text> Water 2</text>
<text x="164" y="278" fill="rgba(255,255,255,0.45)" font-family="system-ui,sans-serif" font-size="9" font-style="italic"> <text x="224" y="338" fill="rgba(255,255,255,0.45)" font-family="system-ui,sans-serif" font-size="9" font-style="italic">
&#x2605; White Sky</text> &#x2605; White Sky</text>
</a> </a>
<a href="/docs/water-3" target="_top"> <a href="/docs/water-3" target="_top">
<circle cx="50" cy="50" r="32" fill="url(#glow-Water)"/> <circle cx="110" cy="110" r="32" fill="url(#glow-Water)"/>
<circle cx="50" cy="50" r="18" fill="#42a5f5cc" stroke="rgba(255,255,255,0.5)" stroke-width="2"/> <circle cx="110" cy="110" r="18" fill="#42a5f5" stroke="rgba(255,255,255,0.4)" stroke-width="2"/>
<text x="74" y="54" fill="rgba(255,255,255,0.9)" font-family="system-ui,sans-serif" font-size="11" font-weight="600"> <text x="134" y="114" fill="rgba(255,255,255,0.9)" font-family="system-ui,sans-serif" font-size="11" font-weight="600">
Water 3</text> Water 3</text>
</a> </a>
<a href="/docs/water-4" target="_top"> <a href="/docs/water-4" target="_top">
<circle cx="640" cy="110" r="32" fill="url(#glow-Water)"/> <circle cx="700" cy="170" r="32" fill="url(#glow-Water)"/>
<circle cx="640" cy="110" r="18" fill="#42a5f5cc" stroke="rgba(255,255,255,0.5)" stroke-width="2"/> <circle cx="700" cy="170" r="18" fill="#42a5f5" stroke="rgba(255,255,255,0.4)" stroke-width="2"/>
<text x="664" y="114" fill="rgba(255,255,255,0.9)" font-family="system-ui,sans-serif" font-size="11" font-weight="600"> <text x="724" y="174" fill="rgba(255,255,255,0.9)" font-family="system-ui,sans-serif" font-size="11" font-weight="600">
Water 4</text> Water 4</text>
</a> </a>
<a href="/docs/water-5" target="_top"> <a href="/docs/water-5" target="_top">
<circle cx="600" cy="400" r="32" fill="url(#glow-Water)"/> <circle cx="660" cy="460" r="32" fill="url(#glow-Water)"/>
<circle cx="600" cy="400" r="18" fill="#42a5f5cc" stroke="rgba(255,255,255,0.5)" stroke-width="2"/> <circle cx="660" cy="460" r="18" fill="#42a5f5" stroke="rgba(255,255,255,0.4)" stroke-width="2"/>
<text x="624" y="404" fill="rgba(255,255,255,0.9)" font-family="system-ui,sans-serif" font-size="11" font-weight="600"> <text x="684" y="464" fill="rgba(255,255,255,0.9)" font-family="system-ui,sans-serif" font-size="11" font-weight="600">
Water 5</text> Water 5</text>
</a> </a>
<rect x="600" y="20" width="140" height="130" rx="6" fill="rgba(0,0,0,0.4)" stroke="rgba(255,255,255,0.08)"/>
<text x="610" y="35" fill="rgba(255,255,255,0.6)" font-family="system-ui,sans-serif" font-size="10" font-weight="600">Legend</text>
<circle cx="614" cy="49" r="5" fill="#4caf50"/>
<text x="626" y="53" fill="rgba(255,255,255,0.7)" font-family="system-ui,sans-serif" font-size="10">Grass</text>
<circle cx="614" cy="71" r="5" fill="#2e7d32"/>
<text x="626" y="75" fill="rgba(255,255,255,0.7)" font-family="system-ui,sans-serif" font-size="10">Forest</text>
<circle cx="614" cy="93" r="5" fill="#78909c"/>
<text x="626" y="97" fill="rgba(255,255,255,0.7)" font-family="system-ui,sans-serif" font-size="10">Mountain</text>
<circle cx="614" cy="115" r="5" fill="#42a5f5"/>
<text x="626" y="119" fill="rgba(255,255,255,0.7)" font-family="system-ui,sans-serif" font-size="10">Water</text>
<circle cx="614" cy="137" r="5" fill="#8d6e63"/>
<text x="626" y="141" fill="rgba(255,255,255,0.7)" font-family="system-ui,sans-serif" font-size="10">Wasteland</text>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 13 KiB

+19 -110
View File
@@ -1,17 +1,5 @@
{ {
"notes": [ "notes": [
{
"slug": "activate-flora-ecology",
"title": "Activate Flora Ecology"
},
{
"slug": "activate-predator-ecology",
"title": "Activate Predator Ecology"
},
{
"slug": "activate-prey-ecology",
"title": "Activate Prey Ecology"
},
{ {
"slug": "artificer", "slug": "artificer",
"title": "Artificer", "title": "Artificer",
@@ -24,20 +12,12 @@
{ {
"slug": "cache-map", "slug": "cache-map",
"title": "Cache Map", "title": "Cache Map",
"category": "Gear", "category": "Gear"
"cost": "1",
"gearCategory": "Tool",
"effect": "Travel: Discard this gear and spend 2 [[Progress]] while in this region to gain 1 [[Tool]].",
"location": "Anywhere"
}, },
{ {
"slug": "card-library", "slug": "card-library",
"title": "Card Library" "title": "Card Library"
}, },
{
"slug": "companion-cards",
"title": "Companion Cards"
},
{ {
"slug": "concoliator", "slug": "concoliator",
"title": "Concoliator", "title": "Concoliator",
@@ -47,11 +27,6 @@
"slug": "contents", "slug": "contents",
"title": "Contents" "title": "Contents"
}, },
{
"slug": "crest",
"title": "Crest",
"category": "EventType"
},
{ {
"slug": "crisis-markers", "slug": "crisis-markers",
"title": "Crisis Markers" "title": "Crisis Markers"
@@ -71,11 +46,7 @@
{ {
"slug": "dolewood-canoe", "slug": "dolewood-canoe",
"title": "Dolewood Canoe", "title": "Dolewood Canoe",
"category": "Gear", "category": "Gear"
"cost": "4",
"gearCategory": "Tool",
"effect": "Boat. Travel: You need 1 less Progress to place trail markers in water regions.",
"location": "[[White Sky]]"
}, },
{ {
"slug": "e03", "slug": "e03",
@@ -86,10 +57,6 @@
"slug": "ecology", "slug": "ecology",
"title": "Ecology" "title": "Ecology"
}, },
{
"slug": "end-turn",
"title": "End Turn"
},
{ {
"slug": "endeavor-tokens", "slug": "endeavor-tokens",
"title": "Endeavor Tokens" "title": "Endeavor Tokens"
@@ -102,10 +69,6 @@
"slug": "event-cards", "slug": "event-cards",
"title": "Event Cards" "title": "Event Cards"
}, },
{
"slug": "event-type",
"title": "Event Type"
},
{ {
"slug": "events", "slug": "events",
"title": "Events" "title": "Events"
@@ -122,11 +85,7 @@
{ {
"slug": "ferinodex", "slug": "ferinodex",
"title": "Ferinodex", "title": "Ferinodex",
"category": "Gear", "category": "Gear"
"cost": "2",
"gearCategory": "Tool",
"effect": "Explore: Once per day, use in the place of 1[[Focus]].",
"location": "Anywhere"
}, },
{ {
"slug": "flora-meeples", "slug": "flora-meeples",
@@ -158,18 +117,13 @@
"category": "Region" "category": "Region"
}, },
{ {
"slug": "forest-regions", "slug": "forest",
"title": "Forest Regions", "title": "Forest"
"category": "Region Type"
}, },
{ {
"slug": "gauzeblade", "slug": "gauzeblade",
"title": "Gauzeblade", "title": "Gauzeblade",
"category": "Gear", "category": "Gear"
"cost": "5",
"gearCategory": "Weapon",
"effect": "Explore: Spend 2 [[Fitness]] before encountering a predator or prey to store it. The stored being can be traded as a gear of value 2.",
"location": "[[Lone Tree Station]]"
}, },
{ {
"slug": "gear-cards", "slug": "gear-cards",
@@ -201,8 +155,8 @@
"category": "Region" "category": "Region"
}, },
{ {
"slug": "grass-regions", "slug": "grasslands",
"title": "Grass Regions" "title": "Grasslands"
}, },
{ {
"slug": "guide", "slug": "guide",
@@ -212,16 +166,16 @@
{ {
"slug": "hidden-trail-map", "slug": "hidden-trail-map",
"title": "Hidden Trail Map", "title": "Hidden Trail Map",
"category": "Gear", "category": "Gear"
"cost": "3",
"gearCategory": "Tool",
"effect": "Prepare: Discard to travel to a nearby region.",
"location": "Anywhere"
}, },
{ {
"slug": "injury-cards", "slug": "injury-cards",
"title": "Injury Cards" "title": "Injury Cards"
}, },
{
"slug": "lake",
"title": "Lake"
},
{ {
"slug": "losing-the-game", "slug": "losing-the-game",
"title": "Losing the Game" "title": "Losing the Game"
@@ -257,17 +211,12 @@
}, },
{ {
"slug": "mountain", "slug": "mountain",
"title": "Mountain", "title": "Mountain"
"category": "EventType"
}, },
{ {
"slug": "paratrepsis-whistle", "slug": "paratrepsis-whistle",
"title": "Paratrepsis Whistle", "title": "Paratrepsis Whistle",
"category": "Gear", "category": "Gear"
"cost": "6",
"gearCategory": "Tool",
"effect": "Explore: You can spend 1 [[Focus]] to prevent suffering 1 injury.",
"location": "[[Lone Tree Station]]"
}, },
{ {
"slug": "paved-roads", "slug": "paved-roads",
@@ -276,17 +225,12 @@
{ {
"slug": "perfect-day-1", "slug": "perfect-day-1",
"title": "Perfect Day 1", "title": "Perfect Day 1",
"category": "Event", "category": "Event"
"effect": "In each region with 3 or more prey, they stampede! Each Ranger in that region suffers 1 injury, then distribute the prey nearby. Shuffle and add 5 random cards from E to the top of the event deck."
}, },
{ {
"slug": "phonoscopic-headset", "slug": "phonoscopic-headset",
"title": "Phonoscopic Headset", "title": "Phonoscopic Headset",
"category": "Gear", "category": "Gear"
"cost": "2",
"gearCategory": "Garment",
"effect": "Explore: Once per day, use in the place of 1 [[Awareness]]",
"location": ""
}, },
{ {
"slug": "player-boards", "slug": "player-boards",
@@ -324,10 +268,6 @@
"slug": "ranger-meeples", "slug": "ranger-meeples",
"title": "Ranger Meeples" "title": "Ranger Meeples"
}, },
{
"slug": "region-types",
"title": "Region Types"
},
{ {
"slug": "research-station", "slug": "research-station",
"title": "Research Station" "title": "Research Station"
@@ -347,11 +287,7 @@
{ {
"slug": "ruins-map", "slug": "ruins-map",
"title": "Ruins Map", "title": "Ruins Map",
"category": "Gear", "category": "Gear"
"cost": "2",
"gearCategory": "Tool",
"effect": "Travel: Retire this gear an spend 2[[Progress]] while in this region to gain 1 [[Artifact]].",
"location": "Anywhere"
}, },
{ {
"slug": "scout", "slug": "scout",
@@ -371,11 +307,6 @@
"slug": "story", "slug": "story",
"title": "Story" "title": "Story"
}, },
{
"slug": "sun",
"title": "Sun",
"category": "EventType"
},
{ {
"slug": "supply", "slug": "supply",
"title": "Supply" "title": "Supply"
@@ -394,10 +325,6 @@
"slug": "terrain-cards", "slug": "terrain-cards",
"title": "Terrain Cards" "title": "Terrain Cards"
}, },
{
"slug": "terrain-deck",
"title": "Terrain Deck"
},
{ {
"slug": "terrain", "slug": "terrain",
"title": "Terrain", "title": "Terrain",
@@ -406,11 +333,7 @@
{ {
"slug": "totem-of-the-irix", "slug": "totem-of-the-irix",
"title": "Totem of the Irix", "title": "Totem of the Irix",
"category": "Gear", "category": "Gear"
"cost": "2",
"gearCategory": "Consumable",
"effect": "Explore: Discard to help a Ranger anywhere.",
"location": "Anywhere"
}, },
{ {
"slug": "trade", "slug": "trade",
@@ -433,10 +356,6 @@
"slug": "traverse", "slug": "traverse",
"title": "Traverse" "title": "Traverse"
}, },
{
"slug": "turn-start",
"title": "Turn Start"
},
{ {
"slug": "unmaintained-roads", "slug": "unmaintained-roads",
"title": "Unmaintained Roads" "title": "Unmaintained Roads"
@@ -483,11 +402,6 @@
"title": "Water 5", "title": "Water 5",
"category": "Region" "category": "Region"
}, },
{
"slug": "water-regions",
"title": "Water Regions",
"category": "Region Type"
},
{ {
"slug": "weather", "slug": "weather",
"title": "Weather" "title": "Weather"
@@ -503,11 +417,6 @@
{ {
"slug": "xp", "slug": "xp",
"title": "XP" "title": "XP"
},
{
"slug": "overview",
"title": "Overview",
"category": "Overview"
} }
] ]
} }
@@ -1 +0,0 @@
Add 1 flora to the activated region.
@@ -1,4 +0,0 @@
Remove 1 prey for each 1 predator on the region. For each 1 prey removed, add one predator to replace it.
If there are no prey in the region, the predator will instead travel to a nearby region in search for prey. They will favour regions that contain the most prey. If no region contains prey they will move to a region that would be closet to getting them to prey.
@@ -1,4 +0,0 @@
Remove 1 flora for each 1 prey on the region. For each 1 flora removed, add one prey to replace it.
If there are no flora in the region, the prey will instead travel to a nearby region in search for flora. They will favour regions that contain the most flora. If no region contains flora they will move to a region that would be closet to getting them to flora.
@@ -1,3 +0,0 @@
Companion cards that can be pulled from the [[Card Library]]. They can be recruitable for [[Spirit]] or other means, and will follow you.
Up to two companions can follow a Ranger, unless stated otherwise, such as by the [[Concoliator]] role.
-6
View File
@@ -1,6 +0,0 @@
---
category: EventType
---
An event that symbolizes something dangerous.
+1 -1
View File
@@ -3,5 +3,5 @@ category: Gear
Cost: 4 Cost: 4
Gear Category: Tool Gear Category: Tool
Effect: "Boat. Travel: You need 1 less Progress to place trail markers in water regions." Effect: "Boat. Travel: You need 1 less Progress to place trail markers in water regions."
Location: "[[White Sky]]" Location: White Sky
--- ---
-3
View File
@@ -1,3 +0,0 @@
At the end of the turn, we move some of the gear out of the [[Market]] and move some gear from the [[Supply]] back in.
After move to the next [[Turn Start]].
-1
View File
@@ -5,4 +5,3 @@ Draw and resolve on event card at the start of each [[Round]].
Event cards spawn [[Crisis]], activate the [[Ecology]], and represent the [[Story]] and [[Weather]] affecting the [[Valley]]. Event cards spawn [[Crisis]], activate the [[Ecology]], and represent the [[Story]] and [[Weather]] affecting the [[Valley]].
-3
View File
@@ -1,3 +0,0 @@
![[Sun]]
![[Mountain]]
![[Crest]]
+1 -1
View File
@@ -1,3 +1,3 @@
[[Sun]] [[Sun]]
[[Mountain]] [[Mountain]]
[[Crest]] [[Seal]]
@@ -1,3 +0,0 @@
---
category: Region Type
---
View File
@@ -1,2 +0,0 @@
Default region that has [[Lone Tree Station]] and [[Spire]].
+2
View File
@@ -0,0 +1,2 @@
Default region that has [[Lone Tree Station]].
View File
-4
View File
@@ -1,4 +0,0 @@
---
category: EventType
---
An event that represents something to do with mountains.
@@ -1,4 +0,0 @@
[[Forest Regions]]
[[Water Regions]]
[[Grass Regions]]
[[Mountain Regions]]
-4
View File
@@ -1,4 +0,0 @@
---
category: EventType
---
An event that was something to do with it being sunny.
-2
View File
@@ -1,3 +1 @@
The deck of gear. Things that can get something, like a tool, and get the top most tool from the supply. The deck of gear. Things that can get something, like a tool, and get the top most tool from the supply.
This might not be what this deck is called. The supply could refer to items leaving the [[Market]].
+4 -4
View File
@@ -3,12 +3,12 @@ count: "198"
--- ---
Cards you draw when exploring the matching region. Cards you draw when exploring the matching region.
Each Terrain type appears to be spread out across 5 regions in the map, the exception being [[Wasteland]] which is just 1 location to the west. Each Terrain type appears to be spread out across 5 regions in the map, the exception being [[Wasteland]] which is just one location to the west.
# Categories # Categories
![[Water Regions]] ![[Lake]]
![[Grass Regions]] ![[Grasslands]]
![[Forest Regions]] ![[Forest]]
![[Mountain]] ![[Mountain]]
![[Wasteland]] ![[Wasteland]]
@@ -1,8 +0,0 @@
A collection of [[Terrain Cards]] that belong to a certain [[Region Types]].
The default Terrain Deck is comprised of a default deck of [[Terrain Cards]].
Additional cards can be added to each Terrain Deck via [[Event Cards]] or other happenstances.
As such, a Terrain Deck will never start with a [[Crisis]], but it will eventually get one.
-33
View File
@@ -1,33 +0,0 @@
Activate an [[Event Cards]].
You can read the flavor text of the card to the group if your into that kind of roleplay.
If the event card has the [[Crisis]] symbol. Activate a random [[Crisis]] from the [[Card Library]] by shuffling it into the appropriate [[Terrain Deck]]
If it has an [[Event Type]] activate existing event markers. They could be on already made [[Crisis]], equipped [[Gear Cards]], [[Injury Cards]] or [[Companion Cards]].
Once all [[Event Type]] triggers are resolved, activate any ecology visible for it's given terrain type.
- [[Activate Predator Ecology]]
- [[Activate Prey Ecology]]
- [[Activate Flora Ecology]]
> You might be concerned about the idea of the world getting swarmed by Predators as the game goes on. This concern seems resolved by being able to remove [[Predator Meeples]] with Gear or random events. [[Predator Meeples]], [[Prey Meeples]], and [[Flora Meeples]] are also used to activate events on [[Terrain Cards]] so they are not purely a negative. They are also the spice of life.
Then read the main contents of the card.
It may contain a description like: Add 3 predators to the area with the most prey. Shuffle and add 5 random cards from [[Card Library]] D to the top of the event deck.
If so, do what it says, add the 3 [[Predator Meeples]] and take 5 random cards from the [[Card Library]] with that D marking.
---
At this point, the Rangers take their turns in any order in the:
- [[Prepare Phase]]
- [[Explore Phase]]
- [[Travel Phase]]
[[End Turn]]
@@ -1,3 +0,0 @@
---
category: Region Type
---
+8 -13
View File
@@ -3,17 +3,13 @@
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<meta content="width=device-width, initial-scale=1.0" name="viewport"/> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Web</title> <title>Web</title>
<base href="/" /> <base href="/" />
<link id="webassembly" rel="preload"/> <link rel="preload" id="webassembly" />
<link href="lib/bootstrap/dist/css/bootstrap.min.css" rel="stylesheet"/> <link rel="stylesheet" href="lib/bootstrap/dist/css/bootstrap.min.css" />
<link href="favicon.png" rel="icon" type="image/png"/> <link rel="stylesheet" href="css/app.css" />
<link rel="preconnect" href="https://fonts.googleapis.com"> <link rel="icon" type="image/png" href="favicon.png" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&family=Outfit:wght@500;700&display=swap" rel="stylesheet">
<link href="_content/Telerik.UI.for.Blazor/css/kendo-theme-default/all.css" rel="stylesheet"/>
<link href="css/app.css" rel="stylesheet"/>
<link href="Web.styles.css" rel="stylesheet" /> <link href="Web.styles.css" rel="stylesheet" />
<script type="importmap"></script> <script type="importmap"></script>
</head> </head>
@@ -21,18 +17,17 @@
<body> <body>
<div id="app"> <div id="app">
<svg class="loading-progress"> <svg class="loading-progress">
<circle cx="50%" cy="50%" r="40%"/> <circle r="40%" cx="50%" cy="50%" />
<circle cx="50%" cy="50%" r="40%"/> <circle r="40%" cx="50%" cy="50%" />
</svg> </svg>
<div class="loading-progress-text"></div> <div class="loading-progress-text"></div>
</div> </div>
<div id="blazor-error-ui"> <div id="blazor-error-ui">
An unhandled error has occurred. An unhandled error has occurred.
<a class="reload" href=".">Reload</a> <a href="." class="reload">Reload</a>
<span class="dismiss">🗙</span> <span class="dismiss">🗙</span>
</div> </div>
<script defer src="_content/Telerik.UI.for.Blazor/js/telerik-blazor.js"></script>
<script src="_framework/blazor.webassembly#[.{fingerprint}].js"></script> <script src="_framework/blazor.webassembly#[.{fingerprint}].js"></script>
</body> </body>
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -224,7 +224,6 @@ h6, h5, h4, h3, h2, h1 {
h1 { h1 {
font-size: calc(1.375rem + 1.5vw); font-size: calc(1.375rem + 1.5vw);
} }
@media (min-width: 1200px) { @media (min-width: 1200px) {
h1 { h1 {
font-size: 2.5rem; font-size: 2.5rem;
@@ -234,7 +233,6 @@ h1 {
h2 { h2 {
font-size: calc(1.325rem + 0.9vw); font-size: calc(1.325rem + 0.9vw);
} }
@media (min-width: 1200px) { @media (min-width: 1200px) {
h2 { h2 {
font-size: 2rem; font-size: 2rem;
@@ -244,7 +242,6 @@ h2 {
h3 { h3 {
font-size: calc(1.3rem + 0.6vw); font-size: calc(1.3rem + 0.6vw);
} }
@media (min-width: 1200px) { @media (min-width: 1200px) {
h3 { h3 {
font-size: 1.75rem; font-size: 1.75rem;
@@ -254,7 +251,6 @@ h3 {
h4 { h4 {
font-size: calc(1.275rem + 0.3vw); font-size: calc(1.275rem + 0.3vw);
} }
@media (min-width: 1200px) { @media (min-width: 1200px) {
h4 { h4 {
font-size: 1.5rem; font-size: 1.5rem;
@@ -355,7 +351,6 @@ a {
color: rgba(var(--bs-link-color-rgb), var(--bs-link-opacity, 1)); color: rgba(var(--bs-link-color-rgb), var(--bs-link-opacity, 1));
text-decoration: underline; text-decoration: underline;
} }
a:hover { a:hover {
--bs-link-color-rgb: var(--bs-link-hover-color-rgb); --bs-link-color-rgb: var(--bs-link-hover-color-rgb);
} }
@@ -380,7 +375,6 @@ pre {
overflow: auto; overflow: auto;
font-size: 0.875em; font-size: 0.875em;
} }
pre code { pre code {
font-size: inherit; font-size: inherit;
color: inherit; color: inherit;
@@ -392,7 +386,6 @@ code {
color: var(--bs-code-color); color: var(--bs-code-color);
word-wrap: break-word; word-wrap: break-word;
} }
a > code { a > code {
color: inherit; color: inherit;
} }
@@ -404,7 +397,6 @@ kbd {
background-color: var(--bs-body-color); background-color: var(--bs-body-color);
border-radius: 0.25rem; border-radius: 0.25rem;
} }
kbd kbd { kbd kbd {
padding: 0; padding: 0;
font-size: 1em; font-size: 1em;
@@ -482,7 +474,6 @@ select {
select { select {
word-wrap: normal; word-wrap: normal;
} }
select:disabled { select:disabled {
opacity: 1; opacity: 1;
} }
@@ -497,7 +488,6 @@ button,
[type=submit] { [type=submit] {
-webkit-appearance: button; -webkit-appearance: button;
} }
button:not(:disabled), button:not(:disabled),
[type=button]:not(:disabled), [type=button]:not(:disabled),
[type=reset]:not(:disabled), [type=reset]:not(:disabled),
@@ -529,13 +519,11 @@ legend {
font-size: calc(1.275rem + 0.3vw); font-size: calc(1.275rem + 0.3vw);
line-height: inherit; line-height: inherit;
} }
@media (min-width: 1200px) { @media (min-width: 1200px) {
legend { legend {
font-size: 1.5rem; font-size: 1.5rem;
} }
} }
legend + * { legend + * {
clear: left; clear: left;
} }
@@ -224,7 +224,6 @@ h6, h5, h4, h3, h2, h1 {
h1 { h1 {
font-size: calc(1.375rem + 1.5vw); font-size: calc(1.375rem + 1.5vw);
} }
@media (min-width: 1200px) { @media (min-width: 1200px) {
h1 { h1 {
font-size: 2.5rem; font-size: 2.5rem;
@@ -234,7 +233,6 @@ h1 {
h2 { h2 {
font-size: calc(1.325rem + 0.9vw); font-size: calc(1.325rem + 0.9vw);
} }
@media (min-width: 1200px) { @media (min-width: 1200px) {
h2 { h2 {
font-size: 2rem; font-size: 2rem;
@@ -244,7 +242,6 @@ h2 {
h3 { h3 {
font-size: calc(1.3rem + 0.6vw); font-size: calc(1.3rem + 0.6vw);
} }
@media (min-width: 1200px) { @media (min-width: 1200px) {
h3 { h3 {
font-size: 1.75rem; font-size: 1.75rem;
@@ -254,7 +251,6 @@ h3 {
h4 { h4 {
font-size: calc(1.275rem + 0.3vw); font-size: calc(1.275rem + 0.3vw);
} }
@media (min-width: 1200px) { @media (min-width: 1200px) {
h4 { h4 {
font-size: 1.5rem; font-size: 1.5rem;
@@ -355,7 +351,6 @@ a {
color: rgba(var(--bs-link-color-rgb), var(--bs-link-opacity, 1)); color: rgba(var(--bs-link-color-rgb), var(--bs-link-opacity, 1));
text-decoration: underline; text-decoration: underline;
} }
a:hover { a:hover {
--bs-link-color-rgb: var(--bs-link-hover-color-rgb); --bs-link-color-rgb: var(--bs-link-hover-color-rgb);
} }
@@ -380,7 +375,6 @@ pre {
overflow: auto; overflow: auto;
font-size: 0.875em; font-size: 0.875em;
} }
pre code { pre code {
font-size: inherit; font-size: inherit;
color: inherit; color: inherit;
@@ -392,7 +386,6 @@ code {
color: var(--bs-code-color); color: var(--bs-code-color);
word-wrap: break-word; word-wrap: break-word;
} }
a > code { a > code {
color: inherit; color: inherit;
} }
@@ -404,7 +397,6 @@ kbd {
background-color: var(--bs-body-color); background-color: var(--bs-body-color);
border-radius: 0.25rem; border-radius: 0.25rem;
} }
kbd kbd { kbd kbd {
padding: 0; padding: 0;
font-size: 1em; font-size: 1em;
@@ -482,7 +474,6 @@ select {
select { select {
word-wrap: normal; word-wrap: normal;
} }
select:disabled { select:disabled {
opacity: 1; opacity: 1;
} }
@@ -497,7 +488,6 @@ button,
[type=submit] { [type=submit] {
-webkit-appearance: button; -webkit-appearance: button;
} }
button:not(:disabled), button:not(:disabled),
[type=button]:not(:disabled), [type=button]:not(:disabled),
[type=reset]:not(:disabled), [type=reset]:not(:disabled),
@@ -529,13 +519,11 @@ legend {
font-size: calc(1.275rem + 0.3vw); font-size: calc(1.275rem + 0.3vw);
line-height: inherit; line-height: inherit;
} }
@media (min-width: 1200px) { @media (min-width: 1200px) {
legend { legend {
font-size: 1.5rem; font-size: 1.5rem;
} }
} }
legend + * { legend + * {
clear: right; clear: right;
} }
@@ -565,7 +553,6 @@ legend + * {
[type="number"] { [type="number"] {
direction: ltr; direction: ltr;
} }
::-webkit-search-decoration { ::-webkit-search-decoration {
-webkit-appearance: none; -webkit-appearance: none;
} }
@@ -604,5 +591,4 @@ progress {
[hidden] { [hidden] {
display: none !important; display: none !important;
} }
/*# sourceMappingURL=bootstrap-reboot.rtl.css.map */ /*# sourceMappingURL=bootstrap-reboot.rtl.css.map */
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -1,3 +1,3 @@
{ {
"theme": "moonstone" "theme": "obsidian"
} }
+135 -46
View File
@@ -4,24 +4,114 @@
"type": "split", "type": "split",
"children": [ "children": [
{ {
"id": "8d7e193d56bb328e", "id": "418aa81bf380daf2",
"type": "tabs", "type": "tabs",
"children": [ "children": [
{ {
"id": "a4348c23136fecb0", "id": "f9c65a18cd3b4b8e",
"type": "leaf", "type": "leaf",
"pinned": true,
"state": { "state": {
"type": "markdown", "type": "markdown",
"state": { "state": {
"file": "Notes/Region Types.md", "file": "_Overview.md",
"mode": "source", "mode": "source",
"source": false "source": false
}, },
"pinned": true,
"icon": "lucide-file", "icon": "lucide-file",
"title": "Region Types" "title": "_Overview"
}
},
{
"id": "87cde5984a735b23",
"type": "leaf",
"pinned": true,
"state": {
"type": "bases",
"state": {
"file": "Bases/_Roles.base",
"viewName": "Table"
},
"pinned": true,
"icon": "lucide-table",
"title": "_Roles"
}
},
{
"id": "610d4da422cb5c59",
"type": "leaf",
"pinned": true,
"state": {
"type": "bases",
"state": {
"file": "Bases/_Gear.base",
"viewName": "Table"
},
"pinned": true,
"icon": "lucide-table",
"title": "_Gear"
}
},
{
"id": "7aa08c31ba9f2e43",
"type": "leaf",
"pinned": true,
"state": {
"type": "bases",
"state": {
"file": "Bases/_Roles.base",
"viewName": "Table"
},
"pinned": true,
"icon": "lucide-table",
"title": "_Roles"
}
},
{
"id": "d213a79cb6445cda",
"type": "leaf",
"pinned": true,
"state": {
"type": "bases",
"state": {
"file": "Bases/Terrain.base",
"viewName": "Table"
},
"pinned": true,
"icon": "lucide-table",
"title": "Terrain"
}
},
{
"id": "93dd7864d3dae537",
"type": "leaf",
"pinned": true,
"state": {
"type": "bases",
"state": {
"file": "Bases/Regions.base",
"viewName": "Table"
},
"pinned": true,
"icon": "lucide-table",
"title": "Regions"
}
},
{
"id": "eb09ddd2e6309f90",
"type": "leaf",
"state": {
"type": "image",
"state": {
"file": "Images/Map.png"
},
"icon": "lucide-image",
"title": "Map"
} }
} }
] ],
"currentTab": 6
} }
], ],
"direction": "vertical" "direction": "vertical"
@@ -53,7 +143,7 @@
"state": { "state": {
"type": "search", "type": "search",
"state": { "state": {
"query": "Region Types", "query": "Explore",
"matchingCase": false, "matchingCase": false,
"explainSearch": false, "explainSearch": false,
"collapseAll": false, "collapseAll": false,
@@ -185,51 +275,50 @@
}, },
"active": "e8ba8e9287dfab25", "active": "e8ba8e9287dfab25",
"lastOpenFiles": [ "lastOpenFiles": [
"Tasks/Simulate the game state.md",
"Notes/Terrain Deck.md",
"Notes/Region Types.md",
"Mountain Regions.md",
"Notes/Activate Prey Ecology.md",
"Notes/Activate Flora Ecology.md",
"Notes/End Turn.md",
"Notes/Forest Regions.md",
"Images/Map 1.png",
"Notes/Water Regions.md",
"Rules.md",
"Notes/Grass Regions.md",
"Tasks/_Tasks.base",
"Untitled",
"Notes/Supply.md",
"Images/Pasted image 20260609163414.png",
"Overview.md",
"Tasks/Generate overview page.md",
"Notes/Event Type.md",
"Notes/Turn Start.md",
"Notes/Event Cards.md",
"Notes/Activate Predator Ecology.md",
"Notes/Concoliator.md",
"Notes/Companion Cards.md",
"Notes/Sun.md",
"Notes/Mountain.md",
"Notes/Crest.md",
"Notes/Terrain Cards.md",
"Images/Pasted image 20260609163839.png",
"Images/Pasted image 20260609163625.png",
"Images/Pasted image 20260609211711.png",
"Images/Pasted image 20260609170335.png",
"Images/Pasted image 20260609170252.png",
"Images/Pasted image 20260609170321.png",
"Images/Market Example.png",
"Notes/Trader.md",
"Bases/_Roles.base",
"Bases/Regions.base", "Bases/Regions.base",
"Images/Map.png", "Tasks/Generate a csharp script to do what you did.md",
"Tasks/Generate a Markdown Website.md",
"_Tasks.base", "_Tasks.base",
"Tasks/Generate a map of regions and how they connect.md",
"Bases/_Roles.base",
"_Overview.md", "_Overview.md",
"Bases/_Gear.base", "Untitled 2.md",
"Tasks", "Tasks",
"Notes/Artificer.md",
"Bases/Terrain.base", "Bases/Terrain.base",
"Notes/Contents.md",
"Notes/Dolewood Canoe.md",
"Bases/_Gear.base",
"Notes/Concoliator.md",
"Notes/Explorer.md",
"Notes/Guide.md",
"Notes/Shaper.md",
"Notes/Shepard.md",
"Notes/Trader.md",
"Bases/Event.base", "Bases/Event.base",
"Notes" "Notes/Awareness.md",
"Notes/Crisis.md",
"Notes/XP Cubes.md",
"Images/Map 1.png",
"Notes",
"Images/Role Example.png",
"Images/Role Example 2.png",
"Images/Table MVP Example.png",
"Notes/Scout.md",
"Images/Pasted image 20260609211711.png",
"Images/Market Deck.png",
"Images/Map.png",
"Images/Event Card Example.png",
"Images/Event Card Example 2.png",
"Images/Pasted image 20260609163414.png",
"Images",
"Notes/Research Station.md",
"Notes/White Sky.md",
"Notes/Wasteland.md",
"Notes/Wasteland 1.md",
"Notes/Mountain 5.md",
"Notes/Mountain 4.md",
"Notes/Mountain 3.md",
"Bases"
] ]
} }
-3
View File
@@ -1,3 +0,0 @@
---
category: Region Type
---
-1
View File
@@ -1 +0,0 @@
Add 1 flora to the activated region.
-4
View File
@@ -1,4 +0,0 @@
Remove 1 prey for each 1 predator on the region. For each 1 prey removed, add one predator to replace it.
If there are no prey in the region, the predator will instead travel to a nearby region in search for prey. They will favour regions that contain the most prey. If no region contains prey they will move to a region that would be closet to getting them to prey.
-4
View File
@@ -1,4 +0,0 @@
Remove 1 flora for each 1 prey on the region. For each 1 flora removed, add one prey to replace it.
If there are no flora in the region, the prey will instead travel to a nearby region in search for flora. They will favour regions that contain the most flora. If no region contains flora they will move to a region that would be closet to getting them to flora.
-3
View File
@@ -1,3 +0,0 @@
Companion cards that can be pulled from the [[Card Library]]. They can be recruitable for [[Spirit]] or other means, and will follow you.
Up to two companions can follow a Ranger, unless stated otherwise, such as by the [[Concoliator]] role.
-6
View File
@@ -1,6 +0,0 @@
---
category: EventType
---
An event that symbolizes something dangerous.
+1 -1
View File
@@ -3,5 +3,5 @@ category: Gear
Cost: 4 Cost: 4
Gear Category: Tool Gear Category: Tool
Effect: "Boat. Travel: You need 1 less Progress to place trail markers in water regions." Effect: "Boat. Travel: You need 1 less Progress to place trail markers in water regions."
Location: "[[White Sky]]" Location: White Sky
--- ---
-3
View File
@@ -1,3 +0,0 @@
At the end of the turn, we move some of the gear out of the [[Market]] and move some gear from the [[Supply]] back in.
After move to the next [[Turn Start]].
-1
View File
@@ -5,4 +5,3 @@ Draw and resolve on event card at the start of each [[Round]].
Event cards spawn [[Crisis]], activate the [[Ecology]], and represent the [[Story]] and [[Weather]] affecting the [[Valley]]. Event cards spawn [[Crisis]], activate the [[Ecology]], and represent the [[Story]] and [[Weather]] affecting the [[Valley]].
-3
View File
@@ -1,3 +0,0 @@
![[Sun]]
![[Mountain]]
![[Crest]]
+1 -1
View File
@@ -1,3 +1,3 @@
[[Sun]] [[Sun]]
[[Mountain]] [[Mountain]]
[[Crest]] [[Seal]]
-3
View File
@@ -1,3 +0,0 @@
---
category: Region Type
---
View File
-2
View File
@@ -1,2 +0,0 @@
Default region that has [[Lone Tree Station]] and [[Spire]].
+2
View File
@@ -0,0 +1,2 @@
Default region that has [[Lone Tree Station]].
View File
-4
View File
@@ -1,4 +0,0 @@
---
category: EventType
---
An event that represents something to do with mountains.
-4
View File
@@ -1,4 +0,0 @@
[[Forest Regions]]
[[Water Regions]]
[[Grass Regions]]
[[Mountain Regions]]
-4
View File
@@ -1,4 +0,0 @@
---
category: EventType
---
An event that was something to do with it being sunny.
-2
View File
@@ -1,3 +1 @@
The deck of gear. Things that can get something, like a tool, and get the top most tool from the supply. The deck of gear. Things that can get something, like a tool, and get the top most tool from the supply.
This might not be what this deck is called. The supply could refer to items leaving the [[Market]].
+4 -4
View File
@@ -3,12 +3,12 @@ count: "198"
--- ---
Cards you draw when exploring the matching region. Cards you draw when exploring the matching region.
Each Terrain type appears to be spread out across 5 regions in the map, the exception being [[Wasteland]] which is just 1 location to the west. Each Terrain type appears to be spread out across 5 regions in the map, the exception being [[Wasteland]] which is just one location to the west.
# Categories # Categories
![[Water Regions]] ![[Lake]]
![[Grass Regions]] ![[Grasslands]]
![[Forest Regions]] ![[Forest]]
![[Mountain]] ![[Mountain]]
![[Wasteland]] ![[Wasteland]]
-8
View File
@@ -1,8 +0,0 @@
A collection of [[Terrain Cards]] that belong to a certain [[Region Types]].
The default Terrain Deck is comprised of a default deck of [[Terrain Cards]].
Additional cards can be added to each Terrain Deck via [[Event Cards]] or other happenstances.
As such, a Terrain Deck will never start with a [[Crisis]], but it will eventually get one.
-33
View File
@@ -1,33 +0,0 @@
Activate an [[Event Cards]].
You can read the flavor text of the card to the group if your into that kind of roleplay.
If the event card has the [[Crisis]] symbol. Activate a random [[Crisis]] from the [[Card Library]] by shuffling it into the appropriate [[Terrain Deck]]
If it has an [[Event Type]] activate existing event markers. They could be on already made [[Crisis]], equipped [[Gear Cards]], [[Injury Cards]] or [[Companion Cards]].
Once all [[Event Type]] triggers are resolved, activate any ecology visible for it's given terrain type.
- [[Activate Predator Ecology]]
- [[Activate Prey Ecology]]
- [[Activate Flora Ecology]]
> You might be concerned about the idea of the world getting swarmed by Predators as the game goes on. This concern seems resolved by being able to remove [[Predator Meeples]] with Gear or random events. [[Predator Meeples]], [[Prey Meeples]], and [[Flora Meeples]] are also used to activate events on [[Terrain Cards]] so they are not purely a negative. They are also the spice of life.
Then read the main contents of the card.
It may contain a description like: Add 3 predators to the area with the most prey. Shuffle and add 5 random cards from [[Card Library]] D to the top of the event deck.
If so, do what it says, add the 3 [[Predator Meeples]] and take 5 random cards from the [[Card Library]] with that D marking.
---
At this point, the Rangers take their turns in any order in the:
- [[Prepare Phase]]
- [[Explore Phase]]
- [[Travel Phase]]
[[End Turn]]

Some files were not shown because too many files have changed in this diff Show More