Vibed deck viewer
This commit is contained in:
+155
-2
@@ -30,7 +30,7 @@ foreach (var file in mdFiles)
|
|||||||
if (endIndex < 0)
|
if (endIndex < 0)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
var frontmatter = content[3..endIndex].Trim();
|
var frontmatter = content[3..endIndex].Trim().Replace("\r\n", "\n").Replace("\r", "\n");
|
||||||
var yaml = ParseYaml(frontmatter);
|
var yaml = ParseYaml(frontmatter);
|
||||||
|
|
||||||
var name = Path.GetFileNameWithoutExtension(file);
|
var name = Path.GetFileNameWithoutExtension(file);
|
||||||
@@ -117,6 +117,75 @@ writer.WriteLine(" ];");
|
|||||||
writer.WriteLine("}");
|
writer.WriteLine("}");
|
||||||
|
|
||||||
Console.WriteLine($"Generated {cards.Count} cards in {generatedFile}");
|
Console.WriteLine($"Generated {cards.Count} cards in {generatedFile}");
|
||||||
|
|
||||||
|
// ── Decks ──
|
||||||
|
var decksDir = Path.Combine(docsDir, "Decks");
|
||||||
|
var deckFiles = Directory.Exists(decksDir) ? Directory.GetFiles(decksDir, "*.md") : [];
|
||||||
|
var decks = new List<DeckData>();
|
||||||
|
|
||||||
|
foreach (var file in deckFiles)
|
||||||
|
{
|
||||||
|
var content = Encoding.UTF8.GetString(File.ReadAllBytes(file));
|
||||||
|
if (!content.StartsWith("---")) continue;
|
||||||
|
var endIndex = content.IndexOf("---", 3, StringComparison.Ordinal);
|
||||||
|
if (endIndex < 0) continue;
|
||||||
|
|
||||||
|
var frontmatter = content[3..endIndex].Trim().Replace("\r\n", "\n").Replace("\r", "\n");
|
||||||
|
var yaml = ParseYaml(frontmatter);
|
||||||
|
|
||||||
|
var name = Path.GetFileNameWithoutExtension(file);
|
||||||
|
var isVisible = yaml.GetValueOrDefault("isVisible") == "true";
|
||||||
|
|
||||||
|
var deck = new DeckData
|
||||||
|
{
|
||||||
|
Name = name,
|
||||||
|
Cards = ParseList(yaml, "cards").Select(s => StripWikiLink(s) ?? "").Where(s => s != "").ToList(),
|
||||||
|
Keycards = ParseList(yaml, "keycards").Select(s => StripWikiLink(s) ?? "").Where(s => s != "").ToList(),
|
||||||
|
Divers = ParseList(yaml, "divers").Select(s => StripWikiLink(s) ?? "").Where(s => s != "").ToList(),
|
||||||
|
Description = StripWikiLinks(NullIfNa(yaml.GetValueOrDefault("description")))?.Replace("\r\n", "\n").Replace("\r", "\n").Replace("\n\n", "\n").Replace("\n\n", "\n"),
|
||||||
|
Factions = ParseList(yaml, "factions").Select(s => StripWikiLink(s) ?? "").Where(s => s != "").ToList(),
|
||||||
|
IsVisible = isVisible,
|
||||||
|
};
|
||||||
|
|
||||||
|
decks.Add(deck);
|
||||||
|
}
|
||||||
|
|
||||||
|
Console.WriteLine($"Parsed {decks.Count} deck files");
|
||||||
|
|
||||||
|
// Generate Decks.g.cs
|
||||||
|
var deckGeneratedFile = Path.Combine(repoRoot, "Chrono", "Web", "Generated", "Decks.g.cs");
|
||||||
|
Directory.CreateDirectory(Path.GetDirectoryName(deckGeneratedFile)!);
|
||||||
|
using var deckWriter = new StreamWriter(deckGeneratedFile, false, Encoding.UTF8);
|
||||||
|
deckWriter.WriteLine("// <auto-generated/>");
|
||||||
|
deckWriter.WriteLine("#nullable enable");
|
||||||
|
deckWriter.WriteLine();
|
||||||
|
deckWriter.WriteLine("namespace Chrono.Model;");
|
||||||
|
deckWriter.WriteLine();
|
||||||
|
deckWriter.WriteLine("public static class DeckDatabase");
|
||||||
|
deckWriter.WriteLine("{");
|
||||||
|
deckWriter.WriteLine(" public static readonly System.Collections.Generic.List<DeckData> Decks =");
|
||||||
|
deckWriter.WriteLine(" [");
|
||||||
|
|
||||||
|
for (var i = 0; i < decks.Count; i++)
|
||||||
|
{
|
||||||
|
var d = decks[i];
|
||||||
|
deckWriter.WriteLine(" new()");
|
||||||
|
deckWriter.WriteLine(" {");
|
||||||
|
WriteProp(deckWriter, "Name", d.Name, 3);
|
||||||
|
WriteListProp(deckWriter, "Cards", d.Cards, 3);
|
||||||
|
WriteListProp(deckWriter, "Keycards", d.Keycards, 3);
|
||||||
|
WriteListProp(deckWriter, "Divers", d.Divers, 3);
|
||||||
|
WriteStrProp(deckWriter, "Description", d.Description, 3);
|
||||||
|
WriteListProp(deckWriter, "Factions", d.Factions, 3);
|
||||||
|
deckWriter.WriteLine($" IsVisible = {(d.IsVisible ? "true" : "false")},");
|
||||||
|
var comma = i < decks.Count - 1 ? "," : "";
|
||||||
|
deckWriter.WriteLine($" }}{comma}");
|
||||||
|
}
|
||||||
|
|
||||||
|
deckWriter.WriteLine(" ];");
|
||||||
|
deckWriter.WriteLine("}");
|
||||||
|
|
||||||
|
Console.WriteLine($"Generated {decks.Count} decks in {deckGeneratedFile}");
|
||||||
return 0;
|
return 0;
|
||||||
|
|
||||||
// --- Helpers ---
|
// --- Helpers ---
|
||||||
@@ -181,10 +250,48 @@ static Dictionary<string, string> ParseYaml(string yaml)
|
|||||||
var lines = yaml.Split('\n');
|
var lines = yaml.Split('\n');
|
||||||
string? currentKey = null;
|
string? currentKey = null;
|
||||||
var listBuffer = new List<string>();
|
var listBuffer = new List<string>();
|
||||||
|
var inBlockScalar = false;
|
||||||
|
string? blockScalarKey = null;
|
||||||
|
var blockScalarLines = new List<string>();
|
||||||
|
string? quoteKey = null;
|
||||||
|
var quoteLines = new List<string>();
|
||||||
|
|
||||||
foreach (var line in lines)
|
foreach (var line in lines)
|
||||||
{
|
{
|
||||||
var trimmed = line.Trim();
|
var trimmed = line.Trim();
|
||||||
|
|
||||||
|
if (inBlockScalar)
|
||||||
|
{
|
||||||
|
if (line.Length == 0 || line[0] == ' ' || line[0] == '\t')
|
||||||
|
{
|
||||||
|
blockScalarLines.Add(trimmed);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (blockScalarKey != null)
|
||||||
|
dict[blockScalarKey] = string.Join("\n", blockScalarLines);
|
||||||
|
inBlockScalar = false;
|
||||||
|
blockScalarKey = null;
|
||||||
|
blockScalarLines.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (quoteKey != null)
|
||||||
|
{
|
||||||
|
if (line.Length == 0 || line[0] == ' ' || line[0] == '\t')
|
||||||
|
{
|
||||||
|
quoteLines.Add(trimmed);
|
||||||
|
if (trimmed.EndsWith("\""))
|
||||||
|
{
|
||||||
|
dict[quoteKey] = string.Join("\n", quoteLines.Select(UnescapeYaml)).TrimEnd('"');
|
||||||
|
quoteKey = null;
|
||||||
|
quoteLines.Clear();
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
dict[quoteKey] = string.Join("\n", quoteLines.Select(UnescapeYaml));
|
||||||
|
quoteKey = null;
|
||||||
|
quoteLines.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
if (trimmed.Length == 0) continue;
|
if (trimmed.Length == 0) continue;
|
||||||
|
|
||||||
if (trimmed.StartsWith("- "))
|
if (trimmed.StartsWith("- "))
|
||||||
@@ -205,16 +312,62 @@ static Dictionary<string, string> ParseYaml(string yaml)
|
|||||||
currentKey = trimmed[..colonIndex].Trim();
|
currentKey = trimmed[..colonIndex].Trim();
|
||||||
var value = trimmed[(colonIndex + 1)..].Trim();
|
var value = trimmed[(colonIndex + 1)..].Trim();
|
||||||
|
|
||||||
if (value.Length == 0) continue;
|
if (value is "|" or "|-")
|
||||||
|
{
|
||||||
|
inBlockScalar = true;
|
||||||
|
blockScalarKey = currentKey;
|
||||||
|
blockScalarLines.Clear();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value.StartsWith("\"") && !value.EndsWith("\""))
|
||||||
|
{
|
||||||
|
quoteKey = currentKey;
|
||||||
|
quoteLines.Clear();
|
||||||
|
quoteLines.Add(value.TrimStart('"'));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
dict[currentKey] = value;
|
dict[currentKey] = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (listBuffer.Count > 0 && currentKey != null)
|
if (listBuffer.Count > 0 && currentKey != null)
|
||||||
dict[currentKey] = string.Join("\n", listBuffer);
|
dict[currentKey] = string.Join("\n", listBuffer);
|
||||||
|
|
||||||
|
if (inBlockScalar && blockScalarKey != null)
|
||||||
|
dict[blockScalarKey] = string.Join("\n", blockScalarLines);
|
||||||
|
|
||||||
|
if (quoteKey != null)
|
||||||
|
dict[quoteKey] = string.Join("\n", quoteLines.Select(UnescapeYaml));
|
||||||
|
|
||||||
return dict;
|
return dict;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static string UnescapeYaml(string s)
|
||||||
|
{
|
||||||
|
var sb = new StringBuilder();
|
||||||
|
for (var i = 0; i < s.Length; i++)
|
||||||
|
{
|
||||||
|
if (s[i] == '\\' && i + 1 < s.Length)
|
||||||
|
{
|
||||||
|
switch (s[i + 1])
|
||||||
|
{
|
||||||
|
case 'r': sb.Append('\r'); i++; break;
|
||||||
|
case 'n': sb.Append('\n'); i++; break;
|
||||||
|
case 't': sb.Append('\t'); i++; break;
|
||||||
|
case '\\': sb.Append('\\'); i++; break;
|
||||||
|
case '"': sb.Append('"'); i++; break;
|
||||||
|
default: sb.Append(s[i]); break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
sb.Append(s[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return sb.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
static void WriteProp(StreamWriter w, string name, string value, int indent)
|
static void WriteProp(StreamWriter w, string name, string value, int indent)
|
||||||
{
|
{
|
||||||
w.WriteLine($"{new string(' ', indent * 4)}{name} = {ToLiteral(value)},");
|
w.WriteLine($"{new string(' ', indent * 4)}{name} = {ToLiteral(value)},");
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
namespace Chrono.Model;
|
||||||
|
|
||||||
|
public class DeckData
|
||||||
|
{
|
||||||
|
public string Name { get; init; } = "";
|
||||||
|
public List<string> Cards { get; init; } = [];
|
||||||
|
public List<string> Keycards { get; init; } = [];
|
||||||
|
public List<string> Divers { get; init; } = [];
|
||||||
|
public string? Description { get; init; }
|
||||||
|
public List<string> Factions { get; init; } = [];
|
||||||
|
public bool IsVisible { get; init; }
|
||||||
|
}
|
||||||
@@ -2028,6 +2028,7 @@ public static class CardDatabase
|
|||||||
ImmortalizeTo = [
|
ImmortalizeTo = [
|
||||||
],
|
],
|
||||||
ImmortalizeFrom = "Gardener Apprentice",
|
ImmortalizeFrom = "Gardener Apprentice",
|
||||||
|
ImmortalizeWhen = "",
|
||||||
},
|
},
|
||||||
new()
|
new()
|
||||||
{
|
{
|
||||||
@@ -2885,6 +2886,14 @@ public static class CardDatabase
|
|||||||
ImageFile = "Overmind's Guilt.png",
|
ImageFile = "Overmind's Guilt.png",
|
||||||
},
|
},
|
||||||
new()
|
new()
|
||||||
|
{
|
||||||
|
Name = "Overpower",
|
||||||
|
Category = "Keyword",
|
||||||
|
Description = "Excess damage beyond the Durability of this Agent's blocker is dealt directly to the enemy core.",
|
||||||
|
Archetypes = [
|
||||||
|
],
|
||||||
|
},
|
||||||
|
new()
|
||||||
{
|
{
|
||||||
Name = "Overseer of Trials",
|
Name = "Overseer of Trials",
|
||||||
Category = "Agent",
|
Category = "Agent",
|
||||||
|
|||||||
@@ -0,0 +1,77 @@
|
|||||||
|
// <auto-generated/>
|
||||||
|
#nullable enable
|
||||||
|
|
||||||
|
namespace Chrono.Model;
|
||||||
|
|
||||||
|
public static class DeckDatabase
|
||||||
|
{
|
||||||
|
public static readonly System.Collections.Generic.List<DeckData> Decks =
|
||||||
|
[
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
Name = "Big Energy",
|
||||||
|
Cards = [
|
||||||
|
"Brilliant Martyr",
|
||||||
|
"Kinetic Absorber",
|
||||||
|
"Hidden Locus",
|
||||||
|
"Suncursed Conduit",
|
||||||
|
"Swashbuckling Diehard",
|
||||||
|
"Debris Collector",
|
||||||
|
"Paradox Flow",
|
||||||
|
"Starfueled Medics",
|
||||||
|
"Lumbering Starseeker",
|
||||||
|
"Novathermal Mining",
|
||||||
|
"Radiant Channeling",
|
||||||
|
"Supernova",
|
||||||
|
"Gunnery Captain",
|
||||||
|
"Lightsteel Colossus",
|
||||||
|
"Devourer Spawn",
|
||||||
|
"Army of the Sun",
|
||||||
|
],
|
||||||
|
Keycards = [
|
||||||
|
"Kinetic Absorber",
|
||||||
|
"Hidden Locus",
|
||||||
|
"Debris Collector",
|
||||||
|
"Lumbering Starseeker",
|
||||||
|
"Lightsteel Colossus",
|
||||||
|
],
|
||||||
|
Divers = [
|
||||||
|
"Peaceful Synthesizer",
|
||||||
|
"Limit Breaker",
|
||||||
|
],
|
||||||
|
Description = "The idea of this deck is to go heavy on stat-efficient cards. Like 2 for a 2/3 Kinetic Absorber, 5/6 Lumbering Starseeker, and 8 for a 9/14 Lightsteel Colossus. Debris Collector is also quite powerful and will Immortalize into a Living Comet 10/10 with Overpower pretty reliably, but it's also your only stat Mute target, so it might be a bit trash if that's popular in the meta.\nYou got Devourer Spawn to help push for lethal. It's going to have a bunch of keywords on it, given how popular timelines are in the meta.\nYou need to Overflow your mana once in a while, or some of your Supernova removal will be useless. Be pass heavy.\nYou're going to win on very low health if you win. Such is life.\nNo card draw, so Army of the Sun is your hope if things go long. At the worst, it's a spell that summons a big unit right away if it goes off.\nStarfueled Medics and Swashbuckling Diehard are just there to take up space and be mid-range drops. And technically, they can be win conditions, I suppose.\nAttack aggressively with Brilliant Martyr. You really want Star Siphon to ramp.\nAs divers, Peaceful Synthesizer is a 1 drop that will quickly turn into a 3/3. Can't get more stat efficient than that. And Limit Breaker is an early-game growing threat that will demand an answer from the enemy.",
|
||||||
|
Factions = [
|
||||||
|
"Sungrace",
|
||||||
|
],
|
||||||
|
IsVisible = true,
|
||||||
|
},
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
Name = "Rewind Me",
|
||||||
|
Cards = [
|
||||||
|
"Curious Acolyte",
|
||||||
|
"Chronicle of the One",
|
||||||
|
"Prayer of Rescue",
|
||||||
|
"Backhand",
|
||||||
|
"Snap Back",
|
||||||
|
"Sunbringer Artillerist",
|
||||||
|
"Sunshock",
|
||||||
|
"Temple Analyst",
|
||||||
|
"Holy Cleaner",
|
||||||
|
"Balance Blade",
|
||||||
|
"Rescind Authorization",
|
||||||
|
"Out of Line",
|
||||||
|
"Divergence Assassin",
|
||||||
|
"Chronal Quarantine",
|
||||||
|
"Holder of the Instruments",
|
||||||
|
],
|
||||||
|
Keycards = [
|
||||||
|
],
|
||||||
|
Divers = [
|
||||||
|
],
|
||||||
|
Factions = [
|
||||||
|
],
|
||||||
|
IsVisible = false,
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
@@ -24,6 +24,11 @@
|
|||||||
<i class="bi bi-people-fill nav-icon"></i> Agents
|
<i class="bi bi-people-fill nav-icon"></i> Agents
|
||||||
</NavLink>
|
</NavLink>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="nav-item px-3">
|
||||||
|
<NavLink class="nav-link" href="decks">
|
||||||
|
<i class="bi bi-journal-text nav-icon"></i> Decks
|
||||||
|
</NavLink>
|
||||||
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,223 @@
|
|||||||
|
@page "/decks/{Name}"
|
||||||
|
|
||||||
|
<PageTitle>@deck?.Name</PageTitle>
|
||||||
|
|
||||||
|
<div class="deck-detail-page">
|
||||||
|
<a class="back-link" href="/decks"><i class="bi bi-arrow-left"></i> Back to Decks</a>
|
||||||
|
|
||||||
|
@if (deck == null)
|
||||||
|
{
|
||||||
|
<div class="empty-state">
|
||||||
|
<i class="bi bi-exclamation-circle"></i>
|
||||||
|
<p>Deck not found.</p>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<article class="deck-article">
|
||||||
|
<header class="deck-header">
|
||||||
|
<h1>@deck.Name</h1>
|
||||||
|
@if (deck.Factions.Count > 0)
|
||||||
|
{
|
||||||
|
<div class="deck-factions">
|
||||||
|
@foreach (var f in deck.Factions)
|
||||||
|
{
|
||||||
|
<span class="faction-badge">@f</span>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
<p class="deck-card-count">@deck.Cards.Count cards</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
@if (deck.Keycards.Count > 0)
|
||||||
|
{
|
||||||
|
<section class="deck-section">
|
||||||
|
<h2><i class="bi bi-star-fill" style="color: var(--gold);"></i> Keycards</h2>
|
||||||
|
<div class="deck-card-row">
|
||||||
|
@foreach (var cardName in deck.Keycards)
|
||||||
|
{
|
||||||
|
var card = LookupCard(cardName);
|
||||||
|
<button class="mini-card-btn" @onclick="() => SelectCard(card)">
|
||||||
|
<div class="mini-card">
|
||||||
|
<div class="mini-card-img">
|
||||||
|
<img src="@(card?.ImagePath ?? "cards/placeholder.png")" alt="@cardName" loading="lazy"/>
|
||||||
|
</div>
|
||||||
|
<div class="mini-card-name">@cardName</div>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (deck.Divers.Count > 0)
|
||||||
|
{
|
||||||
|
<section class="deck-section">
|
||||||
|
<h2><i class="bi bi-shuffle"></i> Divers</h2>
|
||||||
|
<div class="deck-card-row">
|
||||||
|
@foreach (var cardName in deck.Divers)
|
||||||
|
{
|
||||||
|
var card = LookupCard(cardName);
|
||||||
|
<button class="mini-card-btn" @onclick="() => SelectCard(card)">
|
||||||
|
<div class="mini-card">
|
||||||
|
<div class="mini-card-img">
|
||||||
|
<img src="@(card?.ImagePath ?? "cards/placeholder.png")" alt="@cardName" loading="lazy"/>
|
||||||
|
</div>
|
||||||
|
<div class="mini-card-name">@cardName</div>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
}
|
||||||
|
|
||||||
|
<section class="deck-section">
|
||||||
|
<h2><i class="bi bi-collection-fill"></i> Cards</h2>
|
||||||
|
<div class="deck-card-row">
|
||||||
|
@foreach (var cardName in deck.Cards)
|
||||||
|
{
|
||||||
|
var card = LookupCard(cardName);
|
||||||
|
<button class="mini-card-btn" @onclick="() => SelectCard(card)">
|
||||||
|
<div class="mini-card">
|
||||||
|
<div class="mini-card-img">
|
||||||
|
<img src="@(card?.ImagePath ?? "cards/placeholder.png")" alt="@cardName" loading="lazy"/>
|
||||||
|
</div>
|
||||||
|
<div class="mini-card-name">@cardName</div>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
@if (deck.Description != null)
|
||||||
|
{
|
||||||
|
<section class="deck-section description">
|
||||||
|
<h2><i class="bi bi-chat-quote-fill"></i> Description</h2>
|
||||||
|
<div class="deck-description">
|
||||||
|
@foreach (var paragraph in deck.Description.Split('\n', StringSplitOptions.RemoveEmptyEntries))
|
||||||
|
{
|
||||||
|
<p>@paragraph</p>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
}
|
||||||
|
</article>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if (selectedCard != null)
|
||||||
|
{
|
||||||
|
<div class="modal-backdrop" @onclick="CloseDetail"></div>
|
||||||
|
<div class="card-detail">
|
||||||
|
<button class="detail-close" @onclick="CloseDetail"><i class="bi bi-x-lg"></i></button>
|
||||||
|
<div class="detail-layout">
|
||||||
|
<div class="detail-image">
|
||||||
|
<img src="@selectedCard.ImagePath" alt="@selectedCard.Name"/>
|
||||||
|
</div>
|
||||||
|
<div class="detail-info">
|
||||||
|
<div class="detail-header">
|
||||||
|
<h2>@selectedCard.Name</h2>
|
||||||
|
<div class="detail-meta">
|
||||||
|
<span class="meta-badge category @selectedCard.Category?.ToLowerInvariant()">@selectedCard.Category</span>
|
||||||
|
@if (selectedCard.Cost.HasValue)
|
||||||
|
{
|
||||||
|
<span class="meta-badge cost"><i class="bi bi-lightning-fill"></i> @selectedCard.Cost</span>
|
||||||
|
}
|
||||||
|
@if (selectedCard.Attack.HasValue)
|
||||||
|
{
|
||||||
|
<span class="meta-badge attack"><i class="bi bi-crosshair"></i> @selectedCard.Attack</span>
|
||||||
|
}
|
||||||
|
@if (selectedCard.Health.HasValue)
|
||||||
|
{
|
||||||
|
<span class="meta-badge health"><i class="bi bi-heart-fill"></i> @selectedCard.Health</span>
|
||||||
|
}
|
||||||
|
@if (selectedCard.Speed != null)
|
||||||
|
{
|
||||||
|
<span class="meta-badge speed"><i class="bi bi-wind"></i> @selectedCard.Speed</span>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if (selectedCard.Faction != null)
|
||||||
|
{
|
||||||
|
<div class="detail-field">
|
||||||
|
<span class="field-label"><i class="bi bi-flag-fill"></i> Faction</span>
|
||||||
|
<span class="field-value">@selectedCard.Faction</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
@if (selectedCard.Description != null)
|
||||||
|
{
|
||||||
|
<div class="detail-field description">
|
||||||
|
<span class="field-label"><i class="bi bi-chat-quote-fill"></i></span>
|
||||||
|
<span class="field-value">@selectedCard.Description</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
@if (selectedCard.Set != null)
|
||||||
|
{
|
||||||
|
<div class="detail-field">
|
||||||
|
<span class="field-label"><i class="bi bi-collection"></i> Set</span>
|
||||||
|
<span class="field-value">@selectedCard.Set</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
@if (selectedCard.Archetypes is { Count: > 0 })
|
||||||
|
{
|
||||||
|
<div class="detail-field">
|
||||||
|
<span class="field-label"><i class="bi bi-layers-fill"></i> Archetypes</span>
|
||||||
|
<span class="field-value">@string.Join(", ", selectedCard.Archetypes)</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
@if (selectedCard.ImmortalizeWhen != null)
|
||||||
|
{
|
||||||
|
<div class="detail-field">
|
||||||
|
<span class="field-label"><i class="bi bi-star-fill"></i> Immortalize When</span>
|
||||||
|
<span class="field-value">@selectedCard.ImmortalizeWhen</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
@if (selectedCard.HasImmortalize)
|
||||||
|
{
|
||||||
|
<div class="detail-field">
|
||||||
|
<span class="field-label"><i class="bi bi-arrow-right-circle-fill"></i> Immortalizes To</span>
|
||||||
|
<span class="field-value">@string.Join(", ", selectedCard.ImmortalizeTo!)</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
@if (selectedCard.ImmortalizeFrom != null)
|
||||||
|
{
|
||||||
|
<div class="detail-field">
|
||||||
|
<span class="field-label"><i class="bi bi-arrow-left-circle-fill"></i> Immortalizes From</span>
|
||||||
|
<span class="field-value">@selectedCard.ImmortalizeFrom</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
@code {
|
||||||
|
[Parameter]
|
||||||
|
public string Name { get; set; } = "";
|
||||||
|
|
||||||
|
private DeckData? deck;
|
||||||
|
private CardData? selectedCard;
|
||||||
|
|
||||||
|
protected override void OnParametersSet()
|
||||||
|
{
|
||||||
|
var decoded = Uri.UnescapeDataString(Name);
|
||||||
|
deck = DeckDatabase.Decks.FirstOrDefault(d => d.IsVisible && d.Name == decoded);
|
||||||
|
}
|
||||||
|
|
||||||
|
private CardData? LookupCard(string cardName)
|
||||||
|
{
|
||||||
|
return CardDatabase.Cards.FirstOrDefault(c =>
|
||||||
|
string.Equals(c.Name, cardName, StringComparison.OrdinalIgnoreCase));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SelectCard(CardData? card)
|
||||||
|
{
|
||||||
|
selectedCard = card;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CloseDetail()
|
||||||
|
{
|
||||||
|
selectedCard = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,390 @@
|
|||||||
|
.deck-detail-page {
|
||||||
|
max-width: 800px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 0 1rem 3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-link {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.4rem;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
text-decoration: none;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
transition: color var(--transition);
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-link:hover {
|
||||||
|
color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.deck-article {
|
||||||
|
background: var(--bg-surface);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
padding: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.deck-header {
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
padding-bottom: 1.5rem;
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.deck-header h1 {
|
||||||
|
margin: 0 0 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.deck-factions {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.4rem;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.faction-badge {
|
||||||
|
display: inline-block;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 600;
|
||||||
|
padding: 0.2rem 0.7rem;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: var(--bg-elevated);
|
||||||
|
color: var(--accent);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.deck-card-count {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: var(--text-muted);
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.deck-section {
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.deck-section:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.deck-section h2 {
|
||||||
|
font-size: 1.05rem;
|
||||||
|
font-weight: 600;
|
||||||
|
margin: 0 0 1rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.deck-card-row {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mini-card-btn {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
padding: 0;
|
||||||
|
cursor: pointer;
|
||||||
|
font-family: inherit;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mini-card {
|
||||||
|
width: 110px;
|
||||||
|
background: var(--bg-elevated);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
overflow: hidden;
|
||||||
|
transition: border-color var(--transition), transform var(--transition);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mini-card:hover {
|
||||||
|
border-color: var(--accent);
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mini-card-img {
|
||||||
|
width: 110px;
|
||||||
|
height: 100px;
|
||||||
|
overflow: hidden;
|
||||||
|
background: var(--bg-primary);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mini-card-img img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mini-card-name {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 500;
|
||||||
|
padding: 0.35rem 0.4rem;
|
||||||
|
text-align: center;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.deck-description {
|
||||||
|
font-size: 0.95rem;
|
||||||
|
line-height: 1.7;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.deck-description p {
|
||||||
|
margin: 0 0 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.deck-description p:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state {
|
||||||
|
text-align: center;
|
||||||
|
padding: 4rem 1rem;
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state i {
|
||||||
|
font-size: 3rem;
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Detail Modal (shared with Cards page) ── */
|
||||||
|
.modal-backdrop {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
z-index: 1040;
|
||||||
|
background: rgba(0, 0, 0, 0.7);
|
||||||
|
backdrop-filter: blur(4px);
|
||||||
|
animation: fade-in 0.2s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fade-in {
|
||||||
|
from { opacity: 0; }
|
||||||
|
to { opacity: 1; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-detail {
|
||||||
|
position: fixed;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
z-index: 1050;
|
||||||
|
background: var(--bg-surface);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 16px;
|
||||||
|
padding: 0;
|
||||||
|
max-width: 720px;
|
||||||
|
width: 92vw;
|
||||||
|
max-height: 88vh;
|
||||||
|
overflow-y: auto;
|
||||||
|
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.6);
|
||||||
|
color: var(--text-primary);
|
||||||
|
animation: detail-enter 0.25s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes detail-enter {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translate(-50%, -50%) scale(0.92);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translate(-50%, -50%) scale(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-close {
|
||||||
|
position: absolute;
|
||||||
|
top: 0.75rem;
|
||||||
|
right: 0.75rem;
|
||||||
|
z-index: 1;
|
||||||
|
background: rgba(0, 0, 0, 0.5);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
color: var(--text-primary);
|
||||||
|
width: 2rem;
|
||||||
|
height: 2rem;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
transition: all var(--transition);
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-close:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-layout {
|
||||||
|
display: flex;
|
||||||
|
gap: 1.5rem;
|
||||||
|
padding: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-image {
|
||||||
|
flex: 0 0 260px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-image img {
|
||||||
|
width: 100%;
|
||||||
|
border-radius: var(--radius);
|
||||||
|
box-shadow: var(--shadow);
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-info {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-header {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-header h2 {
|
||||||
|
margin: 0 0 0.75rem;
|
||||||
|
font-size: 1.4rem;
|
||||||
|
line-height: 1.3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-meta {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meta-badge {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.3rem;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 600;
|
||||||
|
padding: 0.25rem 0.6rem;
|
||||||
|
border-radius: 100px;
|
||||||
|
background: var(--bg-hover);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.meta-badge.category.agent {
|
||||||
|
background: rgba(79, 195, 247, 0.15);
|
||||||
|
color: #4fc3f7;
|
||||||
|
border-color: rgba(79, 195, 247, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.meta-badge.category.spell {
|
||||||
|
background: rgba(206, 147, 216, 0.15);
|
||||||
|
color: #ce93d8;
|
||||||
|
border-color: rgba(206, 147, 216, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.meta-badge.category.token {
|
||||||
|
background: rgba(255, 213, 79, 0.15);
|
||||||
|
color: #ffd54f;
|
||||||
|
border-color: rgba(255, 213, 79, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.meta-badge.cost {
|
||||||
|
background: rgba(255, 215, 0, 0.12);
|
||||||
|
color: var(--gold);
|
||||||
|
border-color: rgba(255, 215, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.meta-badge.attack {
|
||||||
|
background: rgba(239, 83, 80, 0.15);
|
||||||
|
color: #ef5350;
|
||||||
|
border-color: rgba(239, 83, 80, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.meta-badge.health {
|
||||||
|
background: rgba(102, 187, 106, 0.15);
|
||||||
|
color: #66bb6a;
|
||||||
|
border-color: rgba(102, 187, 106, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.meta-badge.speed {
|
||||||
|
background: rgba(79, 195, 247, 0.12);
|
||||||
|
color: #4fc3f7;
|
||||||
|
border-color: rgba(79, 195, 247, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-field {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
margin-bottom: 0.6rem;
|
||||||
|
font-size: 0.88rem;
|
||||||
|
line-height: 1.45;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-field.description {
|
||||||
|
background: var(--bg-elevated);
|
||||||
|
padding: 0.6rem 0.75rem;
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
border-left: 3px solid var(--accent);
|
||||||
|
margin-top: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field-label {
|
||||||
|
flex-shrink: 0;
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-size: 0.8rem;
|
||||||
|
font-weight: 500;
|
||||||
|
min-width: 7.5rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 0.3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-field.description .field-label {
|
||||||
|
min-width: auto;
|
||||||
|
color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.field-value {
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-field.description .field-value {
|
||||||
|
color: var(--text-primary);
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-detail::-webkit-scrollbar {
|
||||||
|
width: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-detail::-webkit-scrollbar-thumb {
|
||||||
|
background: var(--border);
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.detail-layout {
|
||||||
|
flex-direction: column;
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-image {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
max-width: 180px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-detail {
|
||||||
|
max-height: 90vh;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
@page "/decks"
|
||||||
|
|
||||||
|
<PageTitle>Decks</PageTitle>
|
||||||
|
|
||||||
|
<div class="decks-page">
|
||||||
|
<div class="decks-header">
|
||||||
|
<h1>Decks</h1>
|
||||||
|
<p class="text-secondary">@decks.Count deck@(decks.Count != 1 ? "s" : "")</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="deck-list">
|
||||||
|
@foreach (var deck in decks)
|
||||||
|
{
|
||||||
|
<a class="deck-card" href="/decks/@Uri.EscapeDataString(deck.Name)">
|
||||||
|
<div class="deck-card-body">
|
||||||
|
<h2 class="deck-card-title">@deck.Name</h2>
|
||||||
|
@if (deck.Factions.Count > 0)
|
||||||
|
{
|
||||||
|
<div class="deck-card-factions">
|
||||||
|
@foreach (var f in deck.Factions)
|
||||||
|
{
|
||||||
|
<span class="faction-badge">@f</span>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
<div class="deck-card-meta">
|
||||||
|
<span>@deck.Cards.Count card@(deck.Cards.Count != 1 ? "s" : "")</span>
|
||||||
|
@if (deck.Keycards.Count > 0)
|
||||||
|
{
|
||||||
|
<span>@deck.Keycards.Count keycard@(deck.Keycards.Count != 1 ? "s" : "")</span>
|
||||||
|
}
|
||||||
|
@if (deck.Divers.Count > 0)
|
||||||
|
{
|
||||||
|
<span>@deck.Divers.Count diver@(deck.Divers.Count != 1 ? "s" : "")</span>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@if (deck.Description != null)
|
||||||
|
{
|
||||||
|
<div class="deck-card-excerpt">@Truncate(deck.Description, 120)</div>
|
||||||
|
}
|
||||||
|
</a>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if (decks.Count == 0)
|
||||||
|
{
|
||||||
|
<div class="empty-state">
|
||||||
|
<i class="bi bi-journal-text"></i>
|
||||||
|
<p>No decks available yet.</p>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@code {
|
||||||
|
private List<DeckData> decks = [];
|
||||||
|
|
||||||
|
protected override void OnInitialized()
|
||||||
|
{
|
||||||
|
decks = DeckDatabase.Decks.Where(d => d.IsVisible).ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string Truncate(string text, int maxLength)
|
||||||
|
{
|
||||||
|
if (text.Length <= maxLength) return text;
|
||||||
|
var lastSpace = text.LastIndexOf(' ', maxLength);
|
||||||
|
return text[..(lastSpace > 0 ? lastSpace : maxLength)] + "...";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
.decks-page {
|
||||||
|
max-width: 900px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 0 1rem 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.decks-header {
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.decks-header h1 {
|
||||||
|
margin: 0 0 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.deck-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.deck-card {
|
||||||
|
display: block;
|
||||||
|
background: var(--bg-surface);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
padding: 1.25rem 1.5rem;
|
||||||
|
text-decoration: none;
|
||||||
|
color: inherit;
|
||||||
|
transition: border-color var(--transition), box-shadow var(--transition);
|
||||||
|
}
|
||||||
|
|
||||||
|
.deck-card:hover {
|
||||||
|
border-color: var(--accent);
|
||||||
|
box-shadow: 0 0 20px var(--accent-glow);
|
||||||
|
}
|
||||||
|
|
||||||
|
.deck-card-title {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
font-weight: 600;
|
||||||
|
margin: 0 0 0.5rem;
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.deck-card-factions {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.4rem;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.faction-badge {
|
||||||
|
display: inline-block;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 600;
|
||||||
|
padding: 0.15rem 0.6rem;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: var(--bg-elevated);
|
||||||
|
color: var(--accent);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.deck-card-meta {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.75rem;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.deck-card-meta span + span::before {
|
||||||
|
content: "·";
|
||||||
|
margin-right: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.deck-card-excerpt {
|
||||||
|
margin-top: 0.6rem;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state {
|
||||||
|
text-align: center;
|
||||||
|
padding: 4rem 1rem;
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state i {
|
||||||
|
font-size: 3rem;
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
Vendored
+2
-1
@@ -11,6 +11,7 @@
|
|||||||
"divers": "multitext",
|
"divers": "multitext",
|
||||||
"archetypes": "multitext",
|
"archetypes": "multitext",
|
||||||
"keycards": "multitext",
|
"keycards": "multitext",
|
||||||
"isVisible": "checkbox"
|
"isVisible": "checkbox",
|
||||||
|
"factions": "multitext"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Vendored
+2
-22
@@ -3,27 +3,6 @@
|
|||||||
"id": "e897f013d92d2cb5",
|
"id": "e897f013d92d2cb5",
|
||||||
"type": "split",
|
"type": "split",
|
||||||
"children": [
|
"children": [
|
||||||
{
|
|
||||||
"id": "6cbb242d281fac81",
|
|
||||||
"type": "tabs",
|
|
||||||
"children": [
|
|
||||||
{
|
|
||||||
"id": "16145cbe588c14bd",
|
|
||||||
"type": "leaf",
|
|
||||||
"pinned": true,
|
|
||||||
"state": {
|
|
||||||
"type": "bases",
|
|
||||||
"state": {
|
|
||||||
"file": "_Timeline.base",
|
|
||||||
"viewName": "Table"
|
|
||||||
},
|
|
||||||
"pinned": true,
|
|
||||||
"icon": "lucide-table",
|
|
||||||
"title": "_Timeline"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"id": "02cc741a0e7b6b27",
|
"id": "02cc741a0e7b6b27",
|
||||||
"type": "tabs",
|
"type": "tabs",
|
||||||
@@ -206,9 +185,10 @@
|
|||||||
},
|
},
|
||||||
"active": "c90153d5f925b0d5",
|
"active": "c90153d5f925b0d5",
|
||||||
"lastOpenFiles": [
|
"lastOpenFiles": [
|
||||||
|
"Swashbuckling Diehard.md",
|
||||||
|
"Decks/Big Energy.md",
|
||||||
"_Decks.base",
|
"_Decks.base",
|
||||||
"Decks/Rewind Me.canvas",
|
"Decks/Rewind Me.canvas",
|
||||||
"Decks/Big Energy.md",
|
|
||||||
"Decks/Rewind Me.md",
|
"Decks/Rewind Me.md",
|
||||||
"Overpower.md",
|
"Overpower.md",
|
||||||
"_Keyword.base",
|
"_Keyword.base",
|
||||||
|
|||||||
@@ -26,20 +26,36 @@ keycards:
|
|||||||
- "[[Hidden Locus]]"
|
- "[[Hidden Locus]]"
|
||||||
- "[[Debris Collector]]"
|
- "[[Debris Collector]]"
|
||||||
- "[[Lumbering Starseeker]]"
|
- "[[Lumbering Starseeker]]"
|
||||||
description: |-
|
- "[[Lightsteel Colossus]]"
|
||||||
The idea of this deck is just go heavy on stat efficient cards. Like 2 for a 2/3 [[Kinetic Absorber]] or 5 for a 5/6 [[Lumbering Starseeker]]. [[Debris Collector]] is also quite insane, and will Immortalize into a [[Living Comet]] 10/10 with [[Overpower]] pretty reliably, but it's also your only stat [[Mute]] target, so might be a bit trash if that's popular in the meta.
|
description: "The idea of this deck is to go heavy on stat-efficient cards. Like 2 for a 2/3 [[Kinetic Absorber]], 5/6 [[Lumbering Starseeker]], and 8 for a 9/14 [[Lightsteel Colossus]]. [[Debris Collector]] is also quite powerful and will Immortalize into a [[Living Comet]] 10/10 with [[Overpower]] pretty reliably, but it's also your only stat [[Mute]] target, so it might be a bit trash if that's popular in the meta.\r
|
||||||
|
|
||||||
You got [[Devourer Spawn]] to help push for lethal. It's going to have a bunch of keywords on it given how popular timelines are in the meta.
|
\r
|
||||||
|
|
||||||
You need to Overflow your mana once in awhile or some of your [[Supernova]] removal will be useless. Be pass heavy.
|
You got [[Devourer Spawn]] to help push for lethal. It's going to have a bunch of keywords on it, given how popular timelines are in the meta.\r
|
||||||
|
|
||||||
Your going to win on very low health if you win. Such is life.
|
\r
|
||||||
|
|
||||||
No card draw, so [[Army of the Sun]] is your hope if things go long. At the worst, it's a spell that summons a big unit right away if it goes off.
|
You need to Overflow your mana once in a while, or some of your [[Supernova]] removal will be useless. Be pass heavy.\r
|
||||||
|
|
||||||
[[Starfueled Medics]] and [[Swashbuckling Diehard]] are just there to take up space and be mid range drops. And technically they can be win conditions I suppose.
|
\r
|
||||||
|
|
||||||
Attack aggressively with [[Brilliant Martyr]]. You really want [[Star Siphon]] to ramp.
|
You're going to win on very low health if you win. Such is life.\r
|
||||||
|
|
||||||
As divers, [[Peaceful Synthesizer]] is a 1 drop that will quickly turn into a 3/3. Can't get more stat efficient then that. And [[Limit Breaker]] is an early game growing threat that will demand an answer from the enemy.
|
\r
|
||||||
|
|
||||||
|
No card draw, so [[Army of the Sun]] is your hope if things go long. At the worst, it's a spell that summons a big unit right away if it goes off.\r
|
||||||
|
|
||||||
|
\r
|
||||||
|
|
||||||
|
[[Starfueled Medics]] and [[Swashbuckling Diehard]] are just there to take up space and be mid-range drops. And technically, they can be win conditions, I suppose.\r
|
||||||
|
|
||||||
|
\r
|
||||||
|
|
||||||
|
Attack aggressively with [[Brilliant Martyr]]. You really want [[Star Siphon]] to ramp.\r
|
||||||
|
|
||||||
|
\r
|
||||||
|
|
||||||
|
As divers, [[Peaceful Synthesizer]] is a 1 drop that will quickly turn into a 3/3. Can't get more stat efficient than that. And [[Limit Breaker]] is an early-game growing threat that will demand an answer from the enemy."
|
||||||
|
factions:
|
||||||
|
- "[[Sungrace]]"
|
||||||
---
|
---
|
||||||
|
|||||||
Reference in New Issue
Block a user