Adding a syndicate pairing page with some notes

This commit is contained in:
2026-07-17 17:12:39 -04:00
parent 8fa0422be4
commit e64193639e
50 changed files with 1689 additions and 398 deletions
+21 -6
View File
@@ -49,7 +49,7 @@ foreach (var file in mdFiles)
Attack = ParseInt(yaml, "attack"), Attack = ParseInt(yaml, "attack"),
Health = ParseInt(yaml, "health"), Health = ParseInt(yaml, "health"),
Description = StripWikiLinks(yaml.GetValueOrDefault("description")), Description = StripWikiLinks(yaml.GetValueOrDefault("description")),
Faction = StripWikiLink(yaml.GetValueOrDefault("faction")), Syndicate = StripWikiLink(yaml.GetValueOrDefault("faction")),
Set = StripWikiLink(yaml.GetValueOrDefault("set")), Set = StripWikiLink(yaml.GetValueOrDefault("set")),
Speed = StripWikiLink(yaml.GetValueOrDefault("speed")), Speed = StripWikiLink(yaml.GetValueOrDefault("speed")),
Archetypes = ParseList(yaml, "archetypes").Select(s => StripWikiLink(s) ?? "").Where(s => s != "").ToList(), Archetypes = ParseList(yaml, "archetypes").Select(s => StripWikiLink(s) ?? "").Where(s => s != "").ToList(),
@@ -109,7 +109,7 @@ for (var i = 0; i < cards.Count; i++)
WriteNullProp(writer, "Attack", c.Attack, 3); WriteNullProp(writer, "Attack", c.Attack, 3);
WriteNullProp(writer, "Health", c.Health, 3); WriteNullProp(writer, "Health", c.Health, 3);
WriteStrProp(writer, "Description", c.Description, 3); WriteStrProp(writer, "Description", c.Description, 3);
WriteStrProp(writer, "Faction", c.Faction, 3); WriteStrProp(writer, "Syndicate", c.Syndicate, 3);
WriteStrProp(writer, "Set", c.Set, 3); WriteStrProp(writer, "Set", c.Set, 3);
WriteStrProp(writer, "Speed", c.Speed, 3); WriteStrProp(writer, "Speed", c.Speed, 3);
WriteListProp(writer, "Archetypes", c.Archetypes, 3); WriteListProp(writer, "Archetypes", c.Archetypes, 3);
@@ -157,7 +157,7 @@ foreach (var file in deckFiles)
Divers = ParseList(yaml, "divers").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") Description = StripWikiLinks(NullIfNa(yaml.GetValueOrDefault("description")))?.Replace("\r\n", "\n")
.Replace("\r", "\n").Replace("\n\n", "\n").Replace("\n\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(), Syndicates = ParseList(yaml, "factions").Select(s => StripWikiLink(s) ?? "").Where(s => s != "").ToList(),
IsVisible = isVisible, IsVisible = isVisible,
DeckCode = deckCode, DeckCode = deckCode,
DeckType = yaml.GetValueOrDefault("deckType") DeckType = yaml.GetValueOrDefault("deckType")
@@ -192,7 +192,7 @@ for (var i = 0; i < decks.Count; i++)
WriteListProp(deckWriter, "Keycards", d.Keycards, 3); WriteListProp(deckWriter, "Keycards", d.Keycards, 3);
WriteListProp(deckWriter, "Divers", d.Divers, 3); WriteListProp(deckWriter, "Divers", d.Divers, 3);
WriteStrProp(deckWriter, "Description", d.Description, 3); WriteStrProp(deckWriter, "Description", d.Description, 3);
WriteListProp(deckWriter, "Factions", d.Factions, 3); WriteListProp(deckWriter, "Syndicates", d.Syndicates, 3);
deckWriter.WriteLine($" IsVisible = {(d.IsVisible ? "true" : "false")},"); deckWriter.WriteLine($" IsVisible = {(d.IsVisible ? "true" : "false")},");
WriteStrProp(deckWriter, "DeckCode", d.DeckCode, 3); WriteStrProp(deckWriter, "DeckCode", d.DeckCode, 3);
WriteStrProp(deckWriter, "DeckType", d.DeckType, 3); WriteStrProp(deckWriter, "DeckType", d.DeckType, 3);
@@ -229,19 +229,33 @@ foreach (var file in mdFiles)
} }
} }
var category = yaml.GetValueOrDefault("category") ?? Path.GetFileName(Path.GetDirectoryName(file)) ?? ""; var category = yaml.GetValueOrDefault("category");
if (category == null)
{
var parentDir = Path.GetFileName(Path.GetDirectoryName(file)) ?? "";
category = parentDir;
}
if (excludedCategories.Contains(category)) continue; if (excludedCategories.Contains(category)) continue;
// Also exclude files that look like base templates // Also exclude files that look like base templates
var fileName = Path.GetFileName(file); var fileName = Path.GetFileName(file);
if (fileName.StartsWith("_")) continue; if (fileName.StartsWith("_")) continue;
// Special handling for Syndicate Pairings
List<string>? syndicatesProp = null;
if (category == "Syndicate Pairings")
{
syndicatesProp = ParseList(yaml, "syndicates");
}
docs.Add(new DocData docs.Add(new DocData
{ {
Title = Path.GetFileNameWithoutExtension(file), Title = Path.GetFileNameWithoutExtension(file),
Category = category, Category = category,
Content = body, Content = body,
Frontmatter = yaml Frontmatter = yaml,
Syndicates = syndicatesProp
}); });
} }
@@ -267,6 +281,7 @@ for (var i = 0; i < docs.Count; i++)
WriteProp(docWriter, "Category", d.Category, 3); WriteProp(docWriter, "Category", d.Category, 3);
WriteStrProp(docWriter, "Content", d.Content, 3); WriteStrProp(docWriter, "Content", d.Content, 3);
WriteDictProp(docWriter, "Frontmatter", d.Frontmatter, 3); WriteDictProp(docWriter, "Frontmatter", d.Frontmatter, 3);
WriteListProp(docWriter, "Syndicates", d.Syndicates, 3);
var comma = i < docs.Count - 1 ? "," : ""; var comma = i < docs.Count - 1 ? "," : "";
docWriter.WriteLine($" }}{comma}"); docWriter.WriteLine($" }}{comma}");
} }
+2 -2
View File
@@ -38,11 +38,11 @@
</div> </div>
</div> </div>
@if (Card.Faction != null) @if (Card.Syndicate != null)
{ {
<div class="detail-field"> <div class="detail-field">
<span class="field-label"><i class="bi bi-flag-fill"></i> Faction</span> <span class="field-label"><i class="bi bi-flag-fill"></i> Faction</span>
<span class="field-value">@Card.Faction</span> <span class="field-value">@Card.Syndicate</span>
</div> </div>
} }
+2 -2
View File
@@ -202,7 +202,7 @@
{ {
allCards = CardDatabase.Cards; allCards = CardDatabase.Cards;
factions = allCards factions = allCards
.Select(c => c.Faction) .Select(c => c.Syndicate)
.Where(f => f != null) .Where(f => f != null)
.Distinct() .Distinct()
.OrderBy(f => f) .OrderBy(f => f)
@@ -214,7 +214,7 @@
var primarySet = allCards.Where(c => var primarySet = allCards.Where(c =>
c.MatchesSearch(search) && c.MatchesSearch(search) &&
(categoryFilter == "" || c.Category == categoryFilter) && (categoryFilter == "" || c.Category == categoryFilter) &&
(factionFilter == "" || c.Faction == factionFilter) && (factionFilter == "" || c.Syndicate == factionFilter) &&
(costFilter == "" || c.Cost?.ToString() == costFilter) (costFilter == "" || c.Cost?.ToString() == costFilter)
); );
@@ -279,7 +279,7 @@
private IEnumerable<CardData> BrowserCards => AllCards private IEnumerable<CardData> BrowserCards => AllCards
.Where(c => c.Category is "Agent" or "Spell" or "Immortalized") .Where(c => c.Category is "Agent" or "Spell" or "Immortalized")
.Where(c => categoryFilter == "" || c.Category == categoryFilter) .Where(c => categoryFilter == "" || c.Category == categoryFilter)
.Where(c => factionFilter == "" || c.Faction == factionFilter) .Where(c => factionFilter == "" || c.Syndicate == factionFilter)
.Where(c => search == "" || c.MatchesSearch(search)); .Where(c => search == "" || c.MatchesSearch(search));
private record DeckGroup(CardData Card, int Count); private record DeckGroup(CardData Card, int Count);
@@ -340,8 +340,8 @@
protected override async Task OnParametersSetAsync() protected override async Task OnParametersSetAsync()
{ {
factions = AllCards factions = AllCards
.Where(c => c.Faction != null) .Where(c => c.Syndicate != null)
.Select(c => c.Faction!) .Select(c => c.Syndicate!)
.Distinct() .Distinct()
.OrderBy(f => f) .OrderBy(f => f)
.ToList(); .ToList();
+1 -1
View File
@@ -1,3 +1,3 @@
@page "/test" @page "/test"
<h1>Test Page</h1> <h1>Test Page</h1>
<p>This is a test page.</p> <p>This is a test page.</p>
+1 -1
View File
@@ -16,4 +16,4 @@
@using Telerik.Blazor @using Telerik.Blazor
@using Telerik.Blazor.Components @using Telerik.Blazor.Components
@using Telerik.DataSource @using Telerik.DataSource
@using Cloud.Components.Layout @using Cloud.Components.Layout
+2 -2
View File
@@ -13,7 +13,7 @@ public class CardData
: 0; : 0;
public string? Description { get; init; } public string? Description { get; init; }
public string? Faction { get; init; } public string? Syndicate { get; init; }
public string? Set { get; init; } public string? Set { get; init; }
public string? Speed { get; init; } public string? Speed { get; init; }
public List<string> Archetypes { get; init; } = []; public List<string> Archetypes { get; init; } = [];
@@ -42,7 +42,7 @@ public class CardData
var q = query.Trim().ToLowerInvariant(); var q = query.Trim().ToLowerInvariant();
return Name.ToLowerInvariant().Contains(q) || return Name.ToLowerInvariant().Contains(q) ||
(Description?.ToLowerInvariant().Contains(q) ?? false) || (Description?.ToLowerInvariant().Contains(q) ?? false) ||
(Faction?.ToLowerInvariant().Contains(q) ?? false) || (Syndicate?.ToLowerInvariant().Contains(q) ?? false) ||
Archetypes.Any(a => a.ToLowerInvariant().Contains(q)) || Archetypes.Any(a => a.ToLowerInvariant().Contains(q)) ||
(Name.ToLowerInvariant().Contains("b.o.o.f.") && q.Equals("boof")) || (Name.ToLowerInvariant().Contains("b.o.o.f.") && q.Equals("boof")) ||
(Name.ToLowerInvariant().Contains("boof") && q.Equals("b.o.o.f.")); (Name.ToLowerInvariant().Contains("boof") && q.Equals("b.o.o.f."));
+1 -1
View File
@@ -11,7 +11,7 @@ public class DeckData
public string? DeckCode { get; init; } public string? DeckCode { get; init; }
public string? DeckType { get; init; } public string? DeckType { get; init; }
public List<string> Factions { get; init; } = []; public List<string> Syndicates { get; init; } = [];
public bool IsVisible { get; init; } public bool IsVisible { get; init; }
public bool IsValid => Cards.Count == 40 && Divers.Count <= 2; public bool IsValid => Cards.Count == 40 && Divers.Count <= 2;
+1
View File
@@ -6,4 +6,5 @@ public class DocData
public string Category { get; init; } = ""; public string Category { get; init; } = "";
public string Content { get; init; } = ""; public string Content { get; init; } = "";
public Dictionary<string, string> Frontmatter { get; init; } = []; public Dictionary<string, string> Frontmatter { get; init; } = [];
public List<string>? Syndicates { get; init; }
} }
@@ -1,4 +1,4 @@
@namespace Shared.Components @namespace Shared.Components
@if (Card != null) @if (Card != null)
{ {
@@ -37,11 +37,11 @@
</div> </div>
</div> </div>
@if (Card.Faction != null) @if (Card.Syndicate != null)
{ {
<div class="detail-field"> <div class="detail-field">
<span class="field-label"><i class="bi bi-flag-fill"></i> Faction</span> <span class="field-label"><i class="bi bi-flag-fill"></i> Faction</span>
<span class="field-value">@Card.Faction</span> <span class="field-value">@Card.Syndicate</span>
</div> </div>
} }
+1 -1
View File
@@ -1,4 +1,4 @@
@namespace Shared.Components @namespace Shared.Components
<div class="deck-card-row"> <div class="deck-card-row">
@foreach (var group in Cards.GroupBy(x => x)) @foreach (var group in Cards.GroupBy(x => x))
+1 -1
View File
@@ -1,4 +1,4 @@
@namespace Shared.Components @namespace Shared.Components
<div class="mana-curve"> <div class="mana-curve">
@foreach (var entry in Distribution) @foreach (var entry in Distribution)
File diff suppressed because it is too large Load Diff
+15 -15
View File
@@ -59,7 +59,7 @@ public static class DeckDatabase
"Bloodline Tracker", "Bloodline Tracker",
], ],
Description = "", Description = "",
Factions = [ Syndicates = [
"Splintergleam", "Splintergleam",
"Singularity", "Singularity",
], ],
@@ -123,7 +123,7 @@ public static class DeckDatabase
"Nonlethal Special Forces", "Nonlethal Special Forces",
], ],
Description = "The idea of this deck is to go heavy on stat-efficient cards. Like 2 for a 2/3 Kinetic Absorber,\n5/6 Lumbering Starseeker, and 8 for a 9/14 Lightsteel Colossus.\nYou got Devourer Spawn to help push for lethal. It's going to have a bunch of keywords on it, given how popular\ntimelines 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.\nAnd if you do end up overflowing many times Supernova can refill for mana bar, and it's worth adding some Snap Backs to the deck to use that spell multiple times.\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\nthat. Nonlethal Special Forces is to enable Phasetide for AOE Mute cards given we don't care about stat\nbuffs. Also as it stands, it always seems to punch a above it's weight on the board given the Disarm effect.\nSo ya, Curb the Anomalies can weaken the enemy board, by shutting off all there better stats, activates, and synergies. Hopefully destroy the entire enemy board in combat.\nAnd I suppose Bearer of the Broth. Since the deck has so much raw stats, you will probably find somewhere to heal.\nLike healing Kinetic Absorber so it can keep killing after it Immortalizes.\nAlso in practice Bearer of the Broth has proven to be an insane and reliable draw engine for this deck.\nGunnery Captain helps clear up the board. Tokens are just 1 to 2 cost units that grew to absurd sizes.\nAnd Army of the Sun helps in the mirror match, given how common this deck is due to the precon.", Description = "The idea of this deck is to go heavy on stat-efficient cards. Like 2 for a 2/3 Kinetic Absorber,\n5/6 Lumbering Starseeker, and 8 for a 9/14 Lightsteel Colossus.\nYou got Devourer Spawn to help push for lethal. It's going to have a bunch of keywords on it, given how popular\ntimelines 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.\nAnd if you do end up overflowing many times Supernova can refill for mana bar, and it's worth adding some Snap Backs to the deck to use that spell multiple times.\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\nthat. Nonlethal Special Forces is to enable Phasetide for AOE Mute cards given we don't care about stat\nbuffs. Also as it stands, it always seems to punch a above it's weight on the board given the Disarm effect.\nSo ya, Curb the Anomalies can weaken the enemy board, by shutting off all there better stats, activates, and synergies. Hopefully destroy the entire enemy board in combat.\nAnd I suppose Bearer of the Broth. Since the deck has so much raw stats, you will probably find somewhere to heal.\nLike healing Kinetic Absorber so it can keep killing after it Immortalizes.\nAlso in practice Bearer of the Broth has proven to be an insane and reliable draw engine for this deck.\nGunnery Captain helps clear up the board. Tokens are just 1 to 2 cost units that grew to absurd sizes.\nAnd Army of the Sun helps in the mirror match, given how common this deck is due to the precon.",
Factions = [ Syndicates = [
"Sungrace", "Sungrace",
"Phasetide", "Phasetide",
], ],
@@ -141,7 +141,7 @@ public static class DeckDatabase
Divers = [ Divers = [
], ],
Description = "", Description = "",
Factions = [ Syndicates = [
], ],
IsVisible = false, IsVisible = false,
DeckCode = "", DeckCode = "",
@@ -199,7 +199,7 @@ public static class DeckDatabase
"Traffic Conductor", "Traffic Conductor",
], ],
Description = "", Description = "",
Factions = [ Syndicates = [
"Sungrace", "Sungrace",
"Singularity", "Singularity",
], ],
@@ -261,7 +261,7 @@ public static class DeckDatabase
"Enthusiastic Bot-Poke", "Enthusiastic Bot-Poke",
], ],
Description = "Built deck as a joke, but it kind of feels nuts in limited testing.\nMulligan everything for Radiant Channeling and Supernova. You are hoping that limited removal keeps you alive.\nIf they heavy buff something out from your removal range, you might still have enough reserve to Throw into the Sun that big target.\nIf you survive, start scaling your board with Awakened Security System or Limit Breaker.", Description = "Built deck as a joke, but it kind of feels nuts in limited testing.\nMulligan everything for Radiant Channeling and Supernova. You are hoping that limited removal keeps you alive.\nIf they heavy buff something out from your removal range, you might still have enough reserve to Throw into the Sun that big target.\nIf you survive, start scaling your board with Awakened Security System or Limit Breaker.",
Factions = [ Syndicates = [
"Sungrace", "Sungrace",
"Singularity", "Singularity",
], ],
@@ -319,7 +319,7 @@ public static class DeckDatabase
"Gunnery Captain", "Gunnery Captain",
], ],
Description = "", Description = "",
Factions = [ Syndicates = [
"Phasetide", "Phasetide",
"Sungrace", "Sungrace",
], ],
@@ -379,7 +379,7 @@ public static class DeckDatabase
"Sapling Dryad", "Sapling Dryad",
], ],
Description = "", Description = "",
Factions = [ Syndicates = [
"Lifeblood", "Lifeblood",
"Sapling Dryad", "Sapling Dryad",
], ],
@@ -439,7 +439,7 @@ public static class DeckDatabase
"Bloodline Tracker", "Bloodline Tracker",
], ],
Description = "", Description = "",
Factions = [ Syndicates = [
"Silence", "Silence",
"Splintergleam", "Splintergleam",
], ],
@@ -471,7 +471,7 @@ public static class DeckDatabase
], ],
Divers = [ Divers = [
], ],
Factions = [ Syndicates = [
], ],
IsVisible = false, IsVisible = false,
}, },
@@ -527,7 +527,7 @@ public static class DeckDatabase
"Nonlethal Special Forces", "Nonlethal Special Forces",
], ],
Description = "", Description = "",
Factions = [ Syndicates = [
"Phasetide", "Phasetide",
"Lifeblood", "Lifeblood",
], ],
@@ -589,7 +589,7 @@ public static class DeckDatabase
"Telepathic Conduit", "Telepathic Conduit",
], ],
Description = "The goal of this deck is just to kill Somnus, the Dreaming. And ideally then remove it from the graveyard with Suncursed Conduit.\nI haven't actually matches into Somnus, the Dreaming in my brief testing, so the deck has yet to prove it can kill Somnus, the Dreaming.", Description = "The goal of this deck is just to kill Somnus, the Dreaming. And ideally then remove it from the graveyard with Suncursed Conduit.\nI haven't actually matches into Somnus, the Dreaming in my brief testing, so the deck has yet to prove it can kill Somnus, the Dreaming.",
Factions = [ Syndicates = [
"Sungrace", "Sungrace",
"Silence", "Silence",
], ],
@@ -649,7 +649,7 @@ public static class DeckDatabase
"Bareknuckle Inquisitor", "Bareknuckle Inquisitor",
], ],
Description = "", Description = "",
Factions = [ Syndicates = [
"Silence", "Silence",
"Phasetide", "Phasetide",
], ],
@@ -712,7 +712,7 @@ public static class DeckDatabase
"Scattered Helpers", "Scattered Helpers",
], ],
Description = "Deck needs work, but the idea is to add a lot of timeline stacks, the cast Convergent Pack with Scattered Helpers on the field. Each dying wolf is going to buff the next one, increasing the damage dealt. This deck was made before the revealed new wolves card, but it would obviously help the deck out.", Description = "Deck needs work, but the idea is to add a lot of timeline stacks, the cast Convergent Pack with Scattered Helpers on the field. Each dying wolf is going to buff the next one, increasing the damage dealt. This deck was made before the revealed new wolves card, but it would obviously help the deck out.",
Factions = [ Syndicates = [
"Lifeblood", "Lifeblood",
"Singularity", "Singularity",
], ],
@@ -772,7 +772,7 @@ public static class DeckDatabase
"Devoted Bloodletter", "Devoted Bloodletter",
], ],
Description = "", Description = "",
Factions = [ Syndicates = [
"Sungrace", "Sungrace",
"Splintergleam", "Splintergleam",
], ],
@@ -832,7 +832,7 @@ public static class DeckDatabase
"Redactionist", "Redactionist",
], ],
Description = "", Description = "",
Factions = [ Syndicates = [
"Lifeblood", "Lifeblood",
"Silence", "Silence",
], ],
+239
View File
@@ -18,6 +18,16 @@ public static class DocDatabase
}, },
}, },
new() new()
{
Title = "Deplete",
Category = "Debuff",
Content = "Agent cannot activate or attack. If it's already attacking, it will continue to strike it's target.",
Frontmatter = new()
{
{ "category", "Debuff" },
},
},
new()
{ {
Title = "Core Charges", Title = "Core Charges",
Category = "Currency", Category = "Currency",
@@ -831,6 +841,235 @@ public static class DocDatabase
}, },
}, },
new() new()
{
Title = "Lifeblood - Phasetide",
Category = "Syndicate Pairings",
Content = "Pairing has cards that care about [[Shift]] like [[Heretic Whistleblower]] and [[Convergent Pack]] so you could go that route.\n\nBoth pairings also have access to [[Flourish]], so you could go a board route and use [[Tidal Wave]] as a finisher.",
Frontmatter = new()
{
{ "syndicates", "- Lifeblood\n- Phasetide" },
{ "category", "Syndicate Pairings" },
{ "overlappingMechanics", "- \"[[Shift]]\"" },
},
Syndicates = [
"Lifeblood",
"Phasetide",
],
},
new()
{
Title = "Lifeblood - Silence",
Category = "Syndicate Pairings",
Content = "Typically the [[Somnus, the Dreaming]] pairing. Access to strong cards like [[Sap Sapper]] and [[Sleepy Druid]].\n\nSuppose you could also go [[Evasive]] with removal to protect your creatures from [[Confront]], and have [[Spirit's Lament]] as a top end.",
Frontmatter = new()
{
{ "syndicates", "- Lifeblood\n- Silence" },
{ "category", "Syndicate Pairings" },
},
Syndicates = [
"Lifeblood",
"Silence",
],
},
new()
{
Title = "Lifeblood - Singularity",
Category = "Syndicate Pairings",
Content = "For this pairing, I typically go [[Sprout]]. \n\n[[P.O.G.O]] is another easy one drop to make all your tokens more lethal, and you have a lot of cheap removal like [[Rapid Iteration]] and [[Wake-Up Prod]] to remove blockers that can otherwise slow down your game plan.",
Frontmatter = new()
{
{ "syndicates", "- Lifeblood\n- Singularity" },
{ "category", "Syndicate Pairings" },
},
Syndicates = [
"Lifeblood",
"Singularity",
],
},
new()
{
Title = "Lifeblood - Splintergleam",
Category = "Syndicate Pairings",
Content = "Both syndicates have access to [[Fervor]]. So you could technically go that route.",
Frontmatter = new()
{
{ "syndicates", "- Lifeblood\n- Splintergleam" },
{ "category", "Syndicate Pairings" },
{ "overlappingMechanics", "- \"[[Fervor]]\"" },
},
Syndicates = [
"Lifeblood",
"Splintergleam",
],
},
new()
{
Title = "Lifeblood - Sungrace",
Category = "Syndicate Pairings",
Content = "Both actually have access to [[Flourish]], given [[Limit Breaker]] and [[Soothing Glow]].\n\nSo you could wrong a flourish deck, protect your agents with the massive amount of protection Sungrace and Lifeblood provide, and get a surprise lethal with [[Symbiosis]] to pay off all your flourish triggers.",
Frontmatter = new()
{
{ "syndicates", "- Lifeblood\n- Sungrace" },
{ "category", "Syndicate Pairings" },
},
Syndicates = [
"Lifeblood",
"Sungrace",
],
},
new()
{
Title = "Phasetide - Silence",
Category = "Syndicate Pairings",
Content = "I suppose a control heavily [[The One True Timeline]] deck. \n\n[[Boof, Ever Loyal]] will return to hand the last played spell the current round if it dies. [[Silence]] has a lot of nice removal spells to copy.\n\nAnd I suppose given how easy it is to immortalize your cards with the one true timeline, [[Solemn Attendant]] will always have a target.\n\n[[Paradox Plague]] can be a bit awkward if there are not many timelines in the timeline stack, or bulky enough paradox units on the board, and [[Phasetide]] provides both.\n\nSo mid range [[The One True Timeline]] deck with [[Silence]] utilities and removal.\n\nAnd potential [[The Firm Hand]] can be what turns on your [[Overburdened Scribe]].",
Frontmatter = new()
{
{ "syndicates", "- Phasetide\n- Silence" },
{ "category", "Syndicate Pairings" },
},
Syndicates = [
"Phasetide",
"Silence",
],
},
new()
{
Title = "Phasetide - Singularity",
Category = "Syndicate Pairings",
Content = "The [[Fractal Phantasm]] pairing, given the sheer amount of access [[Phasetide]] has to [[Shift]].",
Frontmatter = new()
{
{ "syndicates", "- Phasetide\n- Singularity" },
{ "category", "Syndicate Pairings" },
},
Syndicates = [
"Phasetide",
"Singularity",
],
},
new()
{
Title = "Phasetide - Splintergleam",
Category = "Syndicate Pairings",
Content = "You have access to bleeding and healing. Seems kind of clear of a gameplan. Bleed everything, and keep your stuff barely alive, while everything your enemy has dies.",
Frontmatter = new()
{
{ "syndicates", "- Phasetide\n- Splintergleam" },
{ "category", "Syndicate Pairings" },
},
Syndicates = [
"Phasetide",
"Splintergleam",
],
},
new()
{
Title = "Phasetide - Sungrace",
Category = "Syndicate Pairings",
Content = "You can [[Snap Back]] your [[Supernova]] for so much mana.\n\nThis is more of the stereotypically heal faction due to all the healing in [[Phasetide]] and [[Starfueled Medics]] to turn your healing more into a win condition.",
Frontmatter = new()
{
{ "syndicates", "- Phasetide\n- Sungrace" },
{ "category", "Syndicate Pairings" },
{ "overlappingMechanics", "- \"[[Heal]]\"" },
},
Syndicates = [
"Phasetide",
"Sungrace",
],
},
new()
{
Title = "Silence - Singularity",
Category = "Syndicate Pairings",
Content = "I like going [[Deplete]]. Singularity has good removal and [[Timestop]]. \n\nIf your deplete plan still fails, you have [[Zorp, Unrecyclable]] to continue the attacking plan forever.",
Frontmatter = new()
{
{ "syndicates", "- Silence\n- Singularity" },
{ "category", "Syndicate Pairings" },
},
Syndicates = [
"Silence",
"Singularity",
],
},
new()
{
Title = "Silence - Splintergleam",
Category = "Syndicate Pairings",
Content = "The [[Precon Telekinetic Burn]] deck. And I like to go that route. So many activate triggers, with [[Telepathic Conduit]], [[Bloodline Tracker]] and [[Ylka, the Headliner]] all in play, you can just blow up your opponent.",
Frontmatter = new()
{
{ "syndicates", "- Silence\n- Splintergleam" },
{ "category", "Syndicate Pairings" },
},
Syndicates = [
"Silence",
"Splintergleam",
],
},
new()
{
Title = "Silence - Sungrace",
Category = "Syndicate Pairings",
Content = "The typical control pairing.\n\nYou can put down one threat at a time, like [[Limit Breaker]], [[Canine Adjutant]], [[Debris Collector]] and [[Jury of the Second Law]].\n\nAll your [[Silence]] spells will kill their threats, plus you can [[Overflow]] for days.\n\n[[Redactionist]] is great graveyard hate. You also have [[Suncursed Conduit]] to shut down [[Fractal Phantasm]]s, so you can uber kill the things you kill.\n\nIf you encounter a [[Devourer Spawn]] or [[Roiling Amalgam]] you can always [[Throw into the Sun]].\n\n[[APEX Starcruise]] is another top board wipe that also has a big body to start doing damage.",
Frontmatter = new()
{
{ "syndicates", "- Silence\n- Sungrace" },
{ "category", "Syndicate Pairings" },
},
Syndicates = [
"Silence",
"Sungrace",
],
},
new()
{
Title = "Singularity - Splintergleam",
Category = "Syndicate Pairings",
Content = "I've seen a person go the [[Singularity]] + [[Splintergleam]] pairing just to have [[Timestop]] to protect their big [[Bleed]] troops that been heavily buffed. Using [[Nascent Clone]] to duplicate [[Fervor]] casts was also a pretty clever idea.",
Frontmatter = new()
{
{ "syndicates", "- Singularity\n- Splintergleam" },
{ "category", "Syndicate Pairings" },
},
Syndicates = [
"Singularity",
"Splintergleam",
],
},
new()
{
Title = "Singularity - Sungrace",
Category = "Syndicate Pairings",
Content = "This pairing has the funny access to discard revive, so you can cheat out something like [[Kyln, the Dynasty]] or the classic [[Devourer Spawn]] early with [[Entropy's End]].",
Frontmatter = new()
{
{ "syndicates", "- Singularity\n- Sungrace" },
{ "category", "Syndicate Pairings" },
},
Syndicates = [
"Singularity",
"Sungrace",
],
},
new()
{
Title = "Splintergleam - Sungrace",
Category = "Syndicate Pairings",
Content = "I saw someone using [[Lightsteel Engineer]] with the entire [[Volcanic Rivers]] package, and I just love it.\n\nTechnically, [[Gentle Frank]] also reduces core damage, but I feel like your going to die before 8 mana.\n\n[[Clarion, Deepest Breath]] turns your [[Volcanic Rivers]] shifts into life gain, but then you need to run the [[Overflow]] package.",
Frontmatter = new()
{
{ "syndicates", "- Splintergleam\n- Sungrace" },
{ "category", "Syndicate Pairings" },
{ "overlappingMechanics", "- \"[[Volcanic Rivers]]\"" },
},
Syndicates = [
"Splintergleam",
"Sungrace",
],
},
new()
{ {
Title = "Deadly Fauna", Title = "Deadly Fauna",
Category = "Timeline", Category = "Timeline",
+1 -1
View File
@@ -42,7 +42,7 @@
@(((CardData)context).StatEfficiency) @(((CardData)context).StatEfficiency)
</Template> </Template>
</GridColumn> </GridColumn>
<GridColumn Field="@nameof(CardData.Faction)" Title="Faction" Width="140px"/> <GridColumn Field="@nameof(CardData.Syndicate)" Title="Faction" Width="140px"/>
<GridColumn Field="@nameof(CardData.Attack)" Title="ATK" Width="120px"/> <GridColumn Field="@nameof(CardData.Attack)" Title="ATK" Width="120px"/>
<GridColumn Field="@nameof(CardData.Health)" Title="HP" Width="120px"/> <GridColumn Field="@nameof(CardData.Health)" Title="HP" Width="120px"/>
<GridColumn Field="@nameof(CardData.Description)" Title="Description" Width="400px"/> <GridColumn Field="@nameof(CardData.Description)" Title="Description" Width="400px"/>
+3 -3
View File
@@ -6,7 +6,7 @@
<HeadContent> <HeadContent>
<meta name="description" <meta name="description"
content="@(deck != null ? $"View details for the {deck.Name} deck. {deck.Cards.Count} cards, {deck.Factions.Count} factions." : "Deck details and card list.")"/> content="@(deck != null ? $"View details for the {deck.Name} deck. {deck.Cards.Count} cards, {deck.Syndicates.Count} factions." : "Deck details and card list.")"/>
</HeadContent> </HeadContent>
<div class="deck-detail-page"> <div class="deck-detail-page">
@@ -26,10 +26,10 @@
<h1>@deck.Name</h1> <h1>@deck.Name</h1>
<b>Deck Code:</b> <b>Deck Code:</b>
<pre>@deck.DeckCode</pre> <pre>@deck.DeckCode</pre>
@if (deck.Factions.Count > 0) @if (deck.Syndicates.Count > 0)
{ {
<div class="deck-factions"> <div class="deck-factions">
@foreach (var f in deck.Factions) @foreach (var f in deck.Syndicates)
{ {
<span class="faction-badge">@f</span> <span class="faction-badge">@f</span>
} }
+2 -2
View File
@@ -27,10 +27,10 @@
<a class="deck-card" href="/decks/@Uri.EscapeDataString(deck.Name)"> <a class="deck-card" href="/decks/@Uri.EscapeDataString(deck.Name)">
<div class="deck-card-body"> <div class="deck-card-body">
<h2 class="deck-card-title">@deck.Name</h2> <h2 class="deck-card-title">@deck.Name</h2>
@if (deck.Factions.Count > 0) @if (deck.Syndicates.Count > 0)
{ {
<div class="deck-card-factions"> <div class="deck-card-factions">
@foreach (var f in deck.Factions) @foreach (var f in deck.Syndicates)
{ {
<span class="faction-badge">@f</span> <span class="faction-badge">@f</span>
} }
+1 -1
View File
@@ -1,4 +1,4 @@
@namespace Shared.Pages @namespace Shared.Pages
@page "/docs" @page "/docs"
<PageTitle>Docs</PageTitle> <PageTitle>Docs</PageTitle>
+38
View File
@@ -1,3 +1,4 @@
@implements IDisposable
@namespace Shared.Pages @namespace Shared.Pages
@page "/home" @page "/home"
@@ -6,6 +7,10 @@
<div class="home-hero"> <div class="home-hero">
<div class="hero-glow"></div> <div class="hero-glow"></div>
<div class="hero-content"> <div class="hero-content">
<div class="free-pack-countdown">
<span class="countdown-label">Free Pack Countdown:</span>
<span class="countdown-value">@_countdownString</span>
</div>
<h1 class="hero-title">Slop Game Reference - Chrono CCG</h1> <h1 class="hero-title">Slop Game Reference - Chrono CCG</h1>
<p class="hero-subtitle"> <p class="hero-subtitle">
AI generated website for reference notes on playing Chrono CCG. AI generated website for reference notes on playing Chrono CCG.
@@ -23,3 +28,36 @@
</div> </div>
</div> </div>
</div> </div>
@code {
private string _countdownString = "00:00:00";
private System.Threading.Timer? _timer;
protected override void OnInitialized()
{
_timer = new System.Threading.Timer(_ =>
{
UpdateCountdown();
InvokeAsync(StateHasChanged);
}, null, 0, 1000);
}
private void UpdateCountdown()
{
var now = DateTime.Now;
var target = now.Date.AddHours(20); // 8 PM today
if (now >= target)
{
target = target.AddDays(1); // 8 PM tomorrow
}
var remaining = target - now;
_countdownString = $"{(int)remaining.TotalHours:D2}:{remaining.Minutes:D2}:{remaining.Seconds:D2}";
}
public void Dispose()
{
_timer?.Dispose();
}
}
+28 -2
View File
@@ -25,6 +25,34 @@
margin: 0 auto; margin: 0 auto;
} }
.free-pack-countdown {
display: inline-flex;
align-items: center;
gap: 0.6rem;
background: rgba(108, 99, 255, 0.1);
border: 1px solid rgba(108, 99, 255, 0.2);
padding: 0.4rem 1rem;
border-radius: 999px;
margin-bottom: 1.5rem;
backdrop-filter: blur(4px);
}
.countdown-label {
font-size: 0.75rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.05em;
color: var(--text-secondary);
}
.countdown-value {
font-size: 0.9rem;
font-weight: 700;
color: var(--accent);
font-family: 'JetBrains Mono', monospace;
min-width: 4.5rem;
}
.hero-badge { .hero-badge {
display: inline-block; display: inline-block;
font-size: 0.75rem; font-size: 0.75rem;
@@ -257,7 +285,6 @@
} }
/* ── Responsive ── */ /* ── Responsive ── */
@
@media (max-width: 900px) { @media (max-width: 900px) {
.home-stats { .home-stats {
grid-template-columns: repeat(3, 1fr); grid-template-columns: repeat(3, 1fr);
@@ -268,7 +295,6 @@
} }
} }
@
@media (max-width: 600px) { @media (max-width: 600px) {
.hero-title { .hero-title {
font-size: 2.2rem; font-size: 2.2rem;
@@ -38,11 +38,11 @@
</div> </div>
</div> </div>
@if (Card.Faction != null) @if (Card.Syndicate != null)
{ {
<div class="detail-field"> <div class="detail-field">
<span class="field-label"><i class="bi bi-flag-fill"></i> Syndicate</span> <span class="field-label"><i class="bi bi-diagram-3-fill"></i> Syndicate</span>
<span class="field-value">@Card.Faction</span> <span class="field-value">@Card.Syndicate</span>
</div> </div>
} }
@@ -176,10 +176,7 @@
} }
.detail-field.description { .detail-field.description {
background: var(--bg-elevated); padding: 0;
padding: 0.6rem 0.75rem;
border-radius: var(--radius-sm);
border-left: 3px solid var(--accent);
margin-top: 0.25rem; margin-top: 0.25rem;
} }
@@ -205,7 +202,6 @@
.detail-field.description .field-value { .detail-field.description .field-value {
color: var(--text-primary); color: var(--text-primary);
font-style: italic;
} }
.card-detail::-webkit-scrollbar { .card-detail::-webkit-scrollbar {
@@ -31,6 +31,11 @@
<i class="bi bi-journal-text nav-icon"></i> Decks <i class="bi bi-journal-text nav-icon"></i> Decks
</NavLink> </NavLink>
</div> </div>
<div class="nav-item px-3">
<NavLink class="nav-link" href="syndicates">
<i class="bi bi-diagram-2-fill nav-icon"></i> Syndicates
</NavLink>
</div>
<div class="nav-item px-3"> <div class="nav-item px-3">
<NavLink class="nav-link" href="docs"> <NavLink class="nav-link" href="docs">
<i class="bi bi-book-fill nav-icon"></i> Docs <i class="bi bi-book-fill nav-icon"></i> Docs
@@ -0,0 +1,139 @@
@using System.Text.RegularExpressions
@namespace Chrono.Components
@if (Doc != null)
{
<div class="modal-backdrop" @onclick="HandleBackdropClick"></div>
<div class="card-detail card-detail-noimg">
<button class="detail-close" @onclick="HandleClose"><i class="bi bi-x-lg"></i></button>
<div class="detail-layout layout-noimg">
<div class="detail-info">
<div class="detail-header">
<div class="syndicate-badges">
@if (Doc.Syndicates != null)
{
foreach (var syn in Doc.Syndicates)
{
<span class="syn-icon @syn.ToLowerInvariant()" title="@syn"></span>
}
}
</div>
<h2>@Doc.Title</h2>
</div>
@if (!string.IsNullOrWhiteSpace(Doc.Content))
{
<div class="detail-field description">
<span class="field-value">@RenderDescription(Doc.Content)</span>
</div>
}
</div>
</div>
</div>
}
@code {
[Parameter] public DocData? Doc { get; set; }
[Parameter] public EventCallback OnClose { get; set; }
[Parameter] public EventCallback<CardData> OnNavigate { get; set; }
private void HandleClose()
{
_ = OnClose.InvokeAsync();
}
private void HandleBackdropClick()
{
_ = OnClose.InvokeAsync();
}
private void Navigate(CardData card)
{
_ = OnNavigate.InvokeAsync(card);
}
private static CardData? LookupCard(string cardName)
{
return CardDatabase.Cards.FirstOrDefault(c =>
string.Equals(c.Name, cardName, StringComparison.OrdinalIgnoreCase));
}
private RenderFragment RenderDescription(string description)
{
return builder =>
{
var cardNames = CardDatabase.Cards
.Select(c => c.Name)
.Where(n => !string.IsNullOrWhiteSpace(n))
.OrderByDescending(n => n.Length)
.ToList();
var seq = 0;
var remaining = description;
while (!string.IsNullOrEmpty(remaining))
{
var bestIdx = -1;
string? bestName = null;
foreach (var name in cardNames)
{
var pattern = $@"\b{Regex.Escape(name)}\b";
var match = Regex.Match(remaining, pattern, RegexOptions.IgnoreCase);
if (match.Success)
{
if (bestIdx == -1 || match.Index < bestIdx)
{
bestIdx = match.Index;
bestName = match.Value;
}
}
}
// Also check for [[Card Name]] format which is common in docs
var bracketMatch = Regex.Match(remaining, @"\[\[(.*?)\]\]");
if (bracketMatch.Success)
{
if (bestIdx == -1 || bracketMatch.Index < bestIdx)
{
bestIdx = bracketMatch.Index;
bestName = bracketMatch.Value;
}
}
if (bestIdx != -1 && bestName != null)
{
if (bestIdx > 0)
builder.AddContent(seq++, remaining[..bestIdx]);
var cleanName = bestName.StartsWith("[[") && bestName.EndsWith("]]")
? bestName.Substring(2, bestName.Length - 4)
: bestName;
var card = LookupCard(cleanName);
if (card != null)
{
builder.OpenElement(seq++, "button");
builder.AddAttribute(seq++, "class", "inline-card-btn");
builder.AddAttribute(seq++, "onclick",
EventCallback.Factory.Create(this, () => Navigate(card)));
builder.AddContent(seq++, cleanName);
builder.CloseElement();
}
else
{
builder.AddContent(seq++, cleanName);
}
remaining = remaining[(bestIdx + bestName.Length)..];
}
else
{
builder.AddContent(seq++, remaining);
remaining = "";
}
}
};
}
}
@@ -0,0 +1,214 @@
.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: 20px;
padding: 0;
max-width: 1100px;
width: 95vw;
height: 90vh;
max-height: 95vh;
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.95);
}
to {
opacity: 1;
transform: translate(-50%, -50%) scale(1);
}
}
.detail-close {
position: absolute;
top: 1rem;
right: 1rem;
z-index: 1;
background: rgba(0, 0, 0, 0.3);
border: 1px solid rgba(255, 255, 255, 0.1);
color: var(--text-primary);
width: 2.2rem;
height: 2.2rem;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
font-size: 0.9rem;
transition: all var(--transition);
}
.detail-close:hover {
background: rgba(255, 255, 255, 0.1);
transform: rotate(90deg);
}
.detail-layout {
display: flex;
padding: 2.5rem 2rem 2rem;
}
.detail-info {
flex: 1;
min-width: 0;
}
.detail-header {
text-align: center;
margin-bottom: 2rem;
}
.syndicate-badges {
display: flex;
justify-content: center;
gap: 0.75rem;
margin-bottom: 1rem;
}
.syn-icon {
width: 32px;
height: 32px;
border-radius: 50%;
background: currentColor;
box-shadow: 0 0 15px currentColor;
border: 2px solid rgba(255, 255, 255, 0.2);
}
.detail-header h2 {
margin: 0 0 0.5rem;
font-size: 1.8rem;
font-weight: 800;
letter-spacing: -0.02em;
background: linear-gradient(135deg, #fff 0%, #aaa 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.detail-meta {
display: flex;
justify-content: center;
}
.meta-badge {
font-size: 0.75rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.05em;
padding: 0.3rem 0.8rem;
border-radius: 4px;
background: rgba(255, 255, 255, 0.05);
color: var(--text-muted);
border: 1px solid var(--border);
}
.detail-field {
margin-bottom: 1.5rem;
}
.detail-field .field-label {
display: block;
color: var(--text-muted);
font-size: 0.75rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.05em;
margin-bottom: 0.5rem;
}
.detail-field .field-value {
color: var(--text-secondary);
font-size: 1rem;
line-height: 1.6;
}
.detail-field.description {
padding: 0;
border-radius: 0;
border: none;
}
.detail-field.description .field-label {
margin-bottom: 0.75rem;
color: var(--accent);
display: flex;
align-items: center;
gap: 0.5rem;
}
.detail-field.description .field-value {
color: var(--text-primary);
display: block;
font-size: 1.1rem;
white-space: pre-wrap;
}
/* Syndicate Colors */
.lifeblood { color: #ef5350; }
.phasetide { color: #2196f3; }
.silence { color: #9e9e9e; }
.singularity { color: #ffffff; }
.splintergleam { color: #4caf50; }
.sungrace { color: #ffeb3b; }
.inline-card-btn {
background: none;
border: none;
padding: 0;
font-family: inherit;
font-size: inherit;
font-weight: 600;
color: var(--accent);
cursor: pointer;
text-decoration: underline;
text-underline-offset: 2px;
text-decoration-color: rgba(108, 99, 255, 0.3);
transition: all var(--transition);
}
.inline-card-btn:hover {
color: #7b73ff;
text-decoration-color: #7b73ff;
}
.card-detail::-webkit-scrollbar {
width: 6px;
}
.card-detail::-webkit-scrollbar-thumb {
background: var(--border);
border-radius: 3px;
}
@media (max-width: 600px) {
.card-detail {
width: 95vw;
max-height: 90vh;
}
.detail-layout {
padding: 2rem 1.25rem;
}
}
+4 -4
View File
@@ -104,7 +104,7 @@
@foreach (var s in syndicateFilter) @foreach (var s in syndicateFilter)
{ {
<span class="filter-chip"> <span class="filter-chip">
<i class="bi bi-flag-fill"></i> @s <i class="bi bi-diagram-3-fill"></i> @s
<button @onclick="() => SetSyndicate(s)"><i class="bi bi-x"></i></button> <button @onclick="() => SetSyndicate(s)"><i class="bi bi-x"></i></button>
</span> </span>
} }
@@ -147,7 +147,7 @@
<div class="card-cost-badge" title="Cost">@card.Cost</div> <div class="card-cost-badge" title="Cost">@card.Cost</div>
} }
@if (card.IsImmortalized || card.HasImmortalize) @if (card.IsImmortalized)
{ {
<div class="card-immortalize-badge" title="@(card.IsImmortalized ? "Immortalized" : "Immortalizes")"> <div class="card-immortalize-badge" title="@(card.IsImmortalized ? "Immortalized" : "Immortalizes")">
<i class="bi bi-star-fill"></i> <i class="bi bi-star-fill"></i>
@@ -253,7 +253,7 @@
{ {
allCards = CardDatabase.Cards; allCards = CardDatabase.Cards;
syndicates = allCards syndicates = allCards
.Select(c => c.Faction) .Select(c => c.Syndicate)
.Where(f => f != null) .Where(f => f != null)
.Distinct() .Distinct()
.OrderBy(f => f) .OrderBy(f => f)
@@ -265,7 +265,7 @@
var primarySet = allCards.Where(c => var primarySet = allCards.Where(c =>
c.MatchesSearch(search) && c.MatchesSearch(search) &&
(categoryFilter == "" || c.Category == categoryFilter) && (categoryFilter == "" || c.Category == categoryFilter) &&
(syndicateFilter.Count == 0 || (c.Faction != null && syndicateFilter.Contains(c.Faction))) && (syndicateFilter.Count == 0 || (c.Syndicate != null && syndicateFilter.Contains(c.Syndicate))) &&
(costFilter == "" || c.Cost?.ToString() == costFilter) (costFilter == "" || c.Cost?.ToString() == costFilter)
); );
-1
View File
@@ -231,7 +231,6 @@
.syndicate-btn.active.lifeblood { border-color: #4caf50; color: #4caf50; } .syndicate-btn.active.lifeblood { border-color: #4caf50; color: #4caf50; }
.syndicate-btn.active.sungrace { border-color: #ff9800; color: #ff9800; } .syndicate-btn.active.sungrace { border-color: #ff9800; color: #ff9800; }
.syndicate-btn.active.splintergleam { border-color: #f44336; color: #f44336; } .syndicate-btn.active.splintergleam { border-color: #f44336; color: #f44336; }
.syndicate-btn.active.eclipse { border-color: #ffea00; color: #ffea00; }
.syn-icon { .syn-icon {
width: 12px; width: 12px;
+66
View File
@@ -0,0 +1,66 @@
@page "/syndicates"
@using Model
@using Chrono.Components
@inject NavigationManager Navigation
<HeadContent>
<meta name="description" content="Explore all syndicate pairings in Chrono CCG."/>
</HeadContent>
<div class="syndicates-container">
<div class="syndicates-header">
<h1>Syndicate Pairings</h1>
<p class="text-secondary">Explore all @pairings.Count combinations of syndicates</p>
</div>
<div class="pairings-grid">
@foreach (var doc in pairings)
{
var s1 = doc.Syndicates?[0] ?? "";
var s2 = doc.Syndicates?[1] ?? "";
<div class="pairing-card" @onclick="() => OpenPairing(doc)">
<div class="pairing-display">
<div class="syndicate-item @s1.ToLowerInvariant()">
<span class="syn-icon"></span>
<span class="syn-name">@s1</span>
</div>
<div class="pairing-separator">
<i class="bi bi-x-lg"></i>
</div>
<div class="syndicate-item @s2.ToLowerInvariant()">
<span class="syn-icon"></span>
<span class="syn-name">@s2</span>
</div>
</div>
<div class="pairing-info">
<span class="pairing-label">Dual-Syndicate</span>
</div>
</div>
}
</div>
</div>
<SyndicatePairDialog Doc="selectedDoc" OnClose="ClosePairing" />
@code {
private List<DocData> pairings = [];
private DocData? selectedDoc;
protected override void OnInitialized()
{
pairings = DocDatabase.Docs
.Where(d => d.Category == "Syndicate Pairings")
.OrderBy(d => d.Title)
.ToList();
}
private void OpenPairing(DocData doc)
{
selectedDoc = doc;
}
private void ClosePairing()
{
selectedDoc = null;
}
}
@@ -0,0 +1,217 @@
.syndicates-container {
max-width: 1200px;
margin: 0 auto;
padding: 2rem;
}
.syndicates-header {
margin-bottom: 2rem;
text-align: center;
}
.pairings-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 1.5rem;
}
.pairing-card {
background: var(--bg-surface);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 1.5rem;
display: flex;
flex-direction: column;
align-items: center;
transition: transform var(--transition), box-shadow var(--transition);
cursor: pointer;
}
.pairing-card:hover {
transform: translateY(-4px);
box-shadow: var(--shadow);
border-color: var(--accent);
}
.pairing-display {
display: flex;
align-items: center;
gap: 1rem;
margin-bottom: 1rem;
width: 100%;
justify-content: center;
}
.syndicate-item {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.5rem;
flex: 1;
}
.syn-icon {
width: 24px;
height: 24px;
border-radius: 50%;
background: currentColor;
box-shadow: 0 0 10px currentColor;
}
.syn-name {
font-size: 0.9rem;
font-weight: 600;
color: var(--text-primary);
}
.pairing-separator {
color: var(--text-muted);
font-size: 0.8rem;
opacity: 0.5;
}
.pairing-info {
border-top: 1px solid var(--border);
padding-top: 0.75rem;
width: 100%;
text-align: center;
}
.pairing-label {
font-size: 0.75rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.05em;
color: var(--text-muted);
}
.syn-icon {
width: 24px;
height: 24px;
border-radius: 50%;
background: currentColor;
box-shadow: 0 0 10px currentColor;
display: inline-block; /* Ensure visibility */
}
/* Syndicate Colors */
.singularity { color: #ffffff; }
.phasetide { color: #2196f3; }
.silence { color: #9c27b0; }
.lifeblood { color: #4caf50; }
.sungrace { color: #ff9800; }
.splintergleam { color: #f44336; }
@media (max-width: 600px) {
.pairings-grid {
grid-template-columns: 1fr;
}
.syndicates-container {
padding: 1rem;
}
}
/* ── Dialog ── */
.pairing-dialog {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 92vw;
max-width: 600px;
max-height: 85vh;
background: #1a1a1a; /* Explicit dark background */
border: 1px solid var(--border);
border-radius: var(--radius-lg);
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.5);
z-index: 1050;
display: flex;
flex-direction: column;
overflow: hidden;
animation: dialog-enter 0.25s ease-out;
}
@keyframes dialog-enter {
from { opacity: 0; transform: translate(-50%, -50%) scale(0.92); }
to { opacity: 1; transform: translate(-50%, -50%) scale(1); }
}
.dialog-close {
position: absolute;
top: 0.75rem;
right: 0.75rem;
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;
z-index: 2;
transition: all var(--transition);
font-size: 0.8rem;
}
.dialog-close:hover {
background: rgba(255, 255, 255, 0.15);
}
.dialog-header {
padding: 2.5rem 1.5rem 1.5rem;
background: #252525; /* Explicit slightly lighter dark */
border-bottom: 1px solid var(--border);
text-align: center;
}
.dialog-header h2 {
margin: 1.25rem 0 0;
font-size: 1.6rem;
font-weight: 700;
color: #ffffff; /* Explicit white */
}
.dialog-body {
padding: 1.5rem;
overflow-y: auto;
flex: 1;
background: #1a1a1a; /* Explicit dark */
}
.doc-content {
line-height: 1.7;
color: #e0e0e0; /* Explicit light gray */
font-size: 1rem;
}
.inline-card-ref {
font-weight: 600;
color: var(--accent);
}
.doc-content p {
margin-bottom: 1.25rem;
}
.empty-state {
text-align: center;
padding: 3rem 1rem;
color: var(--text-muted);
}
.modal-backdrop {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.45);
backdrop-filter: blur(2px);
z-index: 1040;
animation: fade-in 0.2s ease-out;
}
@keyframes fade-in {
from { opacity: 0; }
to { opacity: 1; }
}
+52
View File
@@ -0,0 +1,52 @@
using System.Text.RegularExpressions;
using Microsoft.Playwright;
using Microsoft.Playwright.NUnit;
namespace Tests;
[Parallelizable(ParallelScope.Self)]
[TestFixture]
public class SyndicateTests : AuthenticatedPageTest
{
[Test]
public async Task SyndicatesPage_ShouldOpenDialogOnPairingClick()
{
await Page.GotoAsync(BaseUrl);
await Expect(Page).ToHaveTitleAsync(new Regex("Chrono CCG"));
// Wait for page load
await Page.WaitForSelectorAsync("nav");
// Take a screenshot of the home page
await Page.ScreenshotAsync(new PageScreenshotOptions { Path = "home-page.png" });
// Navigate to syndicates
await Page.GotoAsync($"{BaseUrl}/syndicates");
// Wait for page load and cards to be present
try {
await Page.WaitForSelectorAsync(".pairing-card", new PageWaitForSelectorOptions { Timeout = 10000 });
} catch (Exception) {
System.Console.WriteLine("[DEBUG_LOG] .pairing-card not found. This might be due to environment issues.");
}
var pairingCards = Page.Locator(".pairing-card");
var count = await pairingCards.CountAsync();
if (count > 0)
{
// Click the first pairing card
await pairingCards.First.ClickAsync();
// Wait for dialog (CardDialog uses .card-detail)
await Page.WaitForSelectorAsync(".card-detail", new PageWaitForSelectorOptions { State = WaitForSelectorState.Visible, Timeout = 5000 });
// Verify dialog is visible
var dialog = Page.Locator(".card-detail");
await Expect(dialog).ToBeVisibleAsync();
// Take a screenshot of the dialog
await Page.ScreenshotAsync(new PageScreenshotOptions { Path = "syndicate-dialog.png" });
}
}
}
+2 -1
View File
@@ -17,6 +17,7 @@
"codeCharges": "number", "codeCharges": "number",
"exp": "number", "exp": "number",
"expPer": "number", "expPer": "number",
"size": "number" "size": "number",
"overlappingMechanics": "multitext"
} }
} }
+37 -22
View File
@@ -13,12 +13,26 @@
"state": { "state": {
"type": "markdown", "type": "markdown",
"state": { "state": {
"file": "Breakdown.md", "file": "Syndicate Pairings/Lifeblood - Splintergleam.md",
"mode": "source", "mode": "source",
"source": false "source": false
}, },
"icon": "lucide-file", "icon": "lucide-file",
"title": "Breakdown" "title": "Lifeblood - Splintergleam"
}
},
{
"id": "c7d8467472493004",
"type": "leaf",
"state": {
"type": "markdown",
"state": {
"file": "Deplete.md",
"mode": "source",
"source": false
},
"icon": "lucide-file",
"title": "Deplete"
} }
} }
] ]
@@ -53,7 +67,7 @@
"state": { "state": {
"type": "search", "type": "search",
"state": { "state": {
"query": "...", "query": "Unstoppable Grow",
"matchingCase": false, "matchingCase": false,
"explainSearch": false, "explainSearch": false,
"collapseAll": false, "collapseAll": false,
@@ -185,33 +199,34 @@
}, },
"active": "852a1aaf6b54ec43", "active": "852a1aaf6b54ec43",
"lastOpenFiles": [ "lastOpenFiles": [
"Syndicate Pairings/Lifeblood - Sungrace.md",
"Syndicate Pairings/Lifeblood - Splintergleam.md",
"Syndicate Pairings/Lifeblood - Singularity.md",
"Syndicate Pairings/Phasetide - Silence.md",
"Syndicate Pairings/Phasetide - Singularity.md",
"Syndicate Pairings/Phasetide - Splintergleam.md",
"Syndicate Pairings/Phasetide - Sungrace.md",
"Syndicate Pairings/Silence - Singularity.md",
"Syndicate Pairings/Silence - Splintergleam.md",
"Syndicate Pairings/Silence - Sungrace.md",
"Syndicate Pairings/Singularity - Splintergleam.md",
"Syndicate Pairings/Singularity - Sungrace.md",
"Syndicate Pairings/Splintergleam - Sungrace.md",
"Syndicate Pairings/Lifeblood - Silence.md",
"Syndicate Pairings/Lifeblood - Phasetide.md",
"Spell/[[Lifeblood]]/Unstoppable Growth.md",
"Deplete.md",
"Store/Card Backs.md",
"Agent/[[Sungrace]]/Limit Breaker.md",
"Breakdown.md",
"_Decks.base", "_Decks.base",
"Deck/Precon Midrange.md", "Deck/Precon Midrange.md",
"Deck/Precon Telekinetic Burn.md", "Deck/Precon Telekinetic Burn.md",
"Deck/Precon Sprouts.md", "Deck/Precon Sprouts.md",
"_Debuff.base", "_Debuff.base",
"Breakdown.md",
"Deck/Big Energy.md", "Deck/Big Energy.md",
"Deck/Aggro.md", "Deck/Aggro.md",
"Deck/Kyln Control.md", "Deck/Kyln Control.md",
"Deck/Overflowing Security.md",
"Deck/Rewind Me.md",
"Deck/Somnus Hate.md",
"Deck/Somnus Spam.md",
"Deck/Sunbleed.md",
"Deck/Sudden Wolves.md",
"Deck/Tempo Deplete.md",
"Deck/Example.md",
"Deck/Rewind.md",
"Agent/[[Phasetide]]/Stern Arbiter.md",
"Immortalized/[[Sungrace]]/Time Devourer Soval.md",
"Spell/[[Phasetide]]/By the Numbers.md",
"Immortalized/[[Splintergleam]]/Boatswain Corvus.md",
"Agent/[[Lifeblood]]/Aardvark Precinct Captain.md",
"Agent/[[Lifeblood]]/Egg Tender.md",
"Quest/Daily Wins.md",
"Quest/Deal 30 Damage with Overpower Agents.md",
"Quest/Weekly Wins.md",
"Pass", "Pass",
"_Store.base", "_Store.base",
"_Timeline.base", "_Timeline.base",
+4
View File
@@ -0,0 +1,4 @@
---
category: Debuff
---
Agent cannot activate or attack. If it's already attacking, it will continue to strike it's target.
@@ -3,8 +3,8 @@ name: Unstoppable Growth
category: Spell category: Spell
artist: Julien Fenoglio artist: Julien Fenoglio
rarity: Lost rarity: Lost
cost: 5 cost: 4
description: "An Agent destroys its allies and gains their Strength and Durability." description: An Agent destroys its allies and gains their Strength and Durability.
faction: "[[Lifeblood]]" faction: "[[Lifeblood]]"
set: "[[Core Set]]" set: "[[Core Set]]"
speed: "[[Slow]]" speed: "[[Slow]]"
@@ -0,0 +1,11 @@
---
syndicates:
- Lifeblood
- Phasetide
category: Syndicate Pairings
overlappingMechanics:
- "[[Shift]]"
---
Pairing has cards that care about [[Shift]] like [[Heretic Whistleblower]] and [[Convergent Pack]] so you could go that route.
Both pairings also have access to [[Flourish]], so you could go a board route and use [[Tidal Wave]] as a finisher.
@@ -0,0 +1,10 @@
---
syndicates:
- Lifeblood
- Silence
category: Syndicate Pairings
---
Typically the [[Somnus, the Dreaming]] pairing. Access to strong cards like [[Sap Sapper]] and [[Sleepy Druid]].
Suppose you could also go [[Evasive]] with removal to protect your creatures from [[Confront]], and have [[Spirit's Lament]] as a top end.
@@ -0,0 +1,9 @@
---
syndicates:
- Lifeblood
- Singularity
category: Syndicate Pairings
---
For this pairing, I typically go [[Sprout]].
[[P.O.G.O]] is another easy one drop to make all your tokens more lethal, and you have a lot of cheap removal like [[Rapid Iteration]] and [[Wake-Up Prod]] to remove blockers that can otherwise slow down your game plan.
@@ -0,0 +1,10 @@
---
syndicates:
- Lifeblood
- Splintergleam
category: Syndicate Pairings
overlappingMechanics:
- "[[Fervor]]"
---
Both syndicates have access to [[Fervor]]. So you could technically go that route.
@@ -0,0 +1,9 @@
---
syndicates:
- Lifeblood
- Sungrace
category: Syndicate Pairings
---
Both actually have access to [[Flourish]], given [[Limit Breaker]] and [[Soothing Glow]].
So you could wrong a flourish deck, protect your agents with the massive amount of protection Sungrace and Lifeblood provide, and get a surprise lethal with [[Symbiosis]] to pay off all your flourish triggers.
@@ -0,0 +1,17 @@
---
syndicates:
- Phasetide
- Silence
category: Syndicate Pairings
---
I suppose a control heavily [[The One True Timeline]] deck.
[[Boof, Ever Loyal]] will return to hand the last played spell the current round if it dies. [[Silence]] has a lot of nice removal spells to copy.
And I suppose given how easy it is to immortalize your cards with the one true timeline, [[Solemn Attendant]] will always have a target.
[[Paradox Plague]] can be a bit awkward if there are not many timelines in the timeline stack, or bulky enough paradox units on the board, and [[Phasetide]] provides both.
So mid range [[The One True Timeline]] deck with [[Silence]] utilities and removal.
And potential [[The Firm Hand]] can be what turns on your [[Overburdened Scribe]].
@@ -0,0 +1,7 @@
---
syndicates:
- Phasetide
- Singularity
category: Syndicate Pairings
---
The [[Fractal Phantasm]] pairing, given the sheer amount of access [[Phasetide]] has to [[Shift]].
@@ -0,0 +1,7 @@
---
syndicates:
- Phasetide
- Splintergleam
category: Syndicate Pairings
---
You have access to bleeding and healing. Seems kind of clear of a gameplan. Bleed everything, and keep your stuff barely alive, while everything your enemy has dies.
@@ -0,0 +1,11 @@
---
syndicates:
- Phasetide
- Sungrace
category: Syndicate Pairings
overlappingMechanics:
- "[[Heal]]"
---
You can [[Snap Back]] your [[Supernova]] for so much mana.
This is more of the stereotypically heal faction due to all the healing in [[Phasetide]] and [[Starfueled Medics]] to turn your healing more into a win condition.
@@ -0,0 +1,9 @@
---
syndicates:
- Silence
- Singularity
category: Syndicate Pairings
---
I like going [[Deplete]]. Singularity has good removal and [[Timestop]].
If your deplete plan still fails, you have [[Zorp, Unrecyclable]] to continue the attacking plan forever.
@@ -0,0 +1,7 @@
---
syndicates:
- Silence
- Splintergleam
category: Syndicate Pairings
---
The [[Precon Telekinetic Burn]] deck. And I like to go that route. So many activate triggers, with [[Telepathic Conduit]], [[Bloodline Tracker]] and [[Ylka, the Headliner]] all in play, you can just blow up your opponent.
@@ -0,0 +1,17 @@
---
syndicates:
- Silence
- Sungrace
category: Syndicate Pairings
---
The typical control pairing.
You can put down one threat at a time, like [[Limit Breaker]], [[Canine Adjutant]], [[Debris Collector]] and [[Jury of the Second Law]].
All your [[Silence]] spells will kill their threats, plus you can [[Overflow]] for days.
[[Redactionist]] is great graveyard hate. You also have [[Suncursed Conduit]] to shut down [[Fractal Phantasm]]s, so you can uber kill the things you kill.
If you encounter a [[Devourer Spawn]] or [[Roiling Amalgam]] you can always [[Throw into the Sun]].
[[APEX Starcruise]] is another top board wipe that also has a big body to start doing damage.
@@ -0,0 +1,8 @@
---
syndicates:
- Singularity
- Splintergleam
category: Syndicate Pairings
---
I've seen a person go the [[Singularity]] + [[Splintergleam]] pairing just to have [[Timestop]] to protect their big [[Bleed]] troops that been heavily buffed. Using [[Nascent Clone]] to duplicate [[Fervor]] casts was also a pretty clever idea.
@@ -0,0 +1,7 @@
---
syndicates:
- Singularity
- Sungrace
category: Syndicate Pairings
---
This pairing has the funny access to discard revive, so you can cheat out something like [[Kyln, the Dynasty]] or the classic [[Devourer Spawn]] early with [[Entropy's End]].
@@ -0,0 +1,13 @@
---
syndicates:
- Splintergleam
- Sungrace
category: Syndicate Pairings
overlappingMechanics:
- "[[Volcanic Rivers]]"
---
I saw someone using [[Lightsteel Engineer]] with the entire [[Volcanic Rivers]] package, and I just love it.
Technically, [[Gentle Frank]] also reduces core damage, but I feel like your going to die before 8 mana.
[[Clarion, Deepest Breath]] turns your [[Volcanic Rivers]] shifts into life gain, but then you need to run the [[Overflow]] package.