Adding Syndicate Pages

This commit is contained in:
2026-07-18 13:18:04 -04:00
parent 54e7dc06c0
commit c8d10c19de
35 changed files with 2119 additions and 468 deletions
+6 -1
View File
@@ -223,12 +223,17 @@ foreach (var file in mdFiles)
var fileName = Path.GetFileName(file); var fileName = Path.GetFileName(file);
if (fileName.StartsWith("_")) continue; if (fileName.StartsWith("_")) continue;
// Special handling for Syndicate Pairings // Special handling for Syndicates and Syndicate Pairings
List<string>? syndicatesProp = null; List<string>? syndicatesProp = null;
if (category == "Syndicate Pairings") if (category == "Syndicate Pairings")
{ {
syndicatesProp = ParseList(yaml, "syndicates"); syndicatesProp = ParseList(yaml, "syndicates");
} }
else if (category == "Syndicate" || category == "Faction")
{
syndicatesProp = [Path.GetFileNameWithoutExtension(file)];
category = "Syndicate"; // Normalize Faction to Syndicate
}
docs.Add(new DocData docs.Add(new DocData
{ {
@@ -136,6 +136,14 @@
<div class="card-shimmer"></div> <div class="card-shimmer"></div>
<img src="@card.ImagePath" alt="@card.Name" loading="lazy" <img src="@card.ImagePath" alt="@card.Name" loading="lazy"
onerror="this.src='data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 width=%22200%22 height=%22280%22><rect fill=%22%23222244%22 width=%22200%22 height=%22280%22/><text fill=%22%23686888%22 font-size=%2214%22 x=%22100%22 y=%22140%22 text-anchor=%22middle%22 dominant-baseline=%22middle%22>No Image</text></svg>'"/> onerror="this.src='data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 width=%22200%22 height=%22280%22><rect fill=%22%23222244%22 width=%22200%22 height=%22280%22/><text fill=%22%23686888%22 font-size=%2214%22 x=%22100%22 y=%22140%22 text-anchor=%22middle%22 dominant-baseline=%22middle%22>No Image</text></svg>'"/>
@if (card.IsSpell && !string.IsNullOrEmpty(card.Speed))
{
<div class="card-speed-badge" title="Speed">
<i class="bi bi-wind"></i> @card.Speed
</div>
}
@if (card.IsImmortalized) @if (card.IsImmortalized)
{ {
<div class="card-immortalize-badge" title="Immortalizes"><i class="bi bi-star-fill"></i> <div class="card-immortalize-badge" title="Immortalizes"><i class="bi bi-star-fill"></i>
@@ -477,6 +477,29 @@
border: 1px solid rgba(255, 215, 0, 0.3); border: 1px solid rgba(255, 215, 0, 0.3);
} }
.card-speed-badge {
position: absolute;
top: 0.4rem;
right: 0.4rem;
background: rgba(0, 0, 0, 0.75);
backdrop-filter: blur(4px);
color: #ce93d8; /* Match spell category color */
font-weight: 700;
font-size: 0.65rem;
padding: 0.2rem 0.5rem;
border-radius: 4px;
border: 1px solid rgba(206, 147, 216, 0.3);
z-index: 2;
line-height: 1;
display: flex;
align-items: center;
gap: 0.2rem;
}
.card-speed-badge i {
font-size: 0.75rem;
}
/* ── Card Label ── */ /* ── Card Label ── */
.card-label { .card-label {
display: flex; display: flex;
+3
View File
@@ -19,4 +19,7 @@
<Visible>true</Visible> <Visible>true</Visible>
</Content> </Content>
</ItemGroup> </ItemGroup>
<ItemGroup>
<Folder Update="..\..\chrono.docs\Faction" />
</ItemGroup>
</Project> </Project>
+50 -42
View File
@@ -22,6 +22,14 @@ public static class CardDatabase
], ],
}, },
new() new()
{
Name = "Timeline",
Category = "Rule",
Description = "Provides a passive effect to all players. Only one timeline can be active at a time. The play who shifts to the timeline is considered the owner. The owner can be a bit of a confusing mechanic, but technically it can matter for some cards cares about WHO is triggering the effect.",
Archetypes = [
],
},
new()
{ {
Name = "Core Charges", Name = "Core Charges",
Category = "Currency", Category = "Currency",
@@ -126,48 +134,6 @@ public static class CardDatabase
], ],
}, },
new() new()
{
Name = "Lifeblood",
Category = "Faction",
Archetypes = [
],
},
new()
{
Name = "Phasetide",
Category = "Faction",
Archetypes = [
],
},
new()
{
Name = "Silence",
Category = "Faction",
Archetypes = [
],
},
new()
{
Name = "Singularity",
Category = "Faction",
Archetypes = [
],
},
new()
{
Name = "Splintergleam",
Category = "Faction",
Archetypes = [
],
},
new()
{
Name = "Sungrace",
Category = "Faction",
Archetypes = [
],
},
new()
{ {
Name = "Blitz", Name = "Blitz",
Category = "Keyword", Category = "Keyword",
@@ -583,6 +549,48 @@ public static class CardDatabase
], ],
}, },
new() new()
{
Name = "Lifeblood",
Category = "Syndicate",
Archetypes = [
],
},
new()
{
Name = "Phasetide",
Category = "Syndicate",
Archetypes = [
],
},
new()
{
Name = "Silence",
Category = "Syndicate",
Archetypes = [
],
},
new()
{
Name = "Singularity",
Category = "Syndicate",
Archetypes = [
],
},
new()
{
Name = "Splintergleam",
Category = "Syndicate",
Archetypes = [
],
},
new()
{
Name = "Sungrace",
Category = "Syndicate",
Archetypes = [
],
},
new()
{ {
Name = "Lifeblood - Phasetide", Name = "Lifeblood - Phasetide",
Category = "Syndicate Pairings", Category = "Syndicate Pairings",
+99 -70
View File
@@ -11,7 +11,7 @@ public static class DocDatabase
{ {
Title = "Breakdown", Title = "Breakdown",
Category = "Notes", Category = "Notes",
Content = "| | Lifeblood | Phasetide | Silence | Singularity | Splittergleam | Sungrace |\n|---------------|-----------|-----------|---------|-------------|---------------|----------|\n| Lifeblood | X | Shift | Sprout | Sprouts | Mid Range | Flourish |\n| Phasetide | | X | Rewind | Rewind | Bleed | Overload |\n| Silence | | | X | Deplete | Burn | Control |\n| Singularity | | | | X | Aggro | Disarm |\n| Splittergleam | | | | | X | Fevor |\n| Sungrace | | | | | | X |\n\nCombinations: 15", Content = "| | Lifeblood | Phasetide | Silence | Singularity | Splittergleam | Sungrace |\r\n|---------------|-----------|-----------|---------|-------------|---------------|----------|\r\n| Lifeblood | X | Shift | Sprout | Sprouts | Mid Range | Flourish |\r\n| Phasetide | | X | Rewind | Rewind | Bleed | Overload |\r\n| Silence | | | X | Deplete | Burn | Control |\r\n| Singularity | | | | X | Aggro | Disarm |\r\n| Splittergleam | | | | | X | Fevor |\r\n| Sungrace | | | | | | X |\r\n\r\nCombinations: 15",
Frontmatter = new() Frontmatter = new()
{ {
{ "category", "Notes" }, { "category", "Notes" },
@@ -28,6 +28,17 @@ public static class DocDatabase
}, },
}, },
new() new()
{
Title = "Timeline",
Category = "Rule",
Content = "",
Frontmatter = new()
{
{ "category", "Rule" },
{ "description", "Provides a passive effect to all players. Only one timeline can be active at a time. The play who shifts to the timeline is considered the owner. The owner can be a bit of a confusing mechanic, but technically it can matter for some cards cares about WHO is triggering the effect." },
},
},
new()
{ {
Title = "Core Charges", Title = "Core Charges",
Category = "Currency", Category = "Currency",
@@ -171,66 +182,6 @@ public static class DocDatabase
}, },
}, },
new() new()
{
Title = "Lifeblood",
Category = "Faction",
Content = "Concepts:\r\n\r\n- Wide Boards\r\n- Overpower",
Frontmatter = new()
{
{ "category", "Faction" },
},
},
new()
{
Title = "Phasetide",
Category = "Faction",
Content = "Concepts:\r\n\r\n- Healing\r\n-",
Frontmatter = new()
{
{ "category", "Faction" },
},
},
new()
{
Title = "Silence",
Category = "Faction",
Content = "Concepts:\r\n\r\n- Activatable Agents\r\n-",
Frontmatter = new()
{
{ "category", "Faction" },
},
},
new()
{
Title = "Singularity",
Category = "Faction",
Content = "Concepts:\r\n\r\n- Discard",
Frontmatter = new()
{
{ "category", "Faction" },
},
},
new()
{
Title = "Splintergleam",
Category = "Faction",
Content = "# Self Damage\r\n\r\nCan hurt it's own units for power. Has [[Bleed]] to make reoccurring damage on units very easy.\r\n\r\n# Core Damage\r\n\r\nCan hurt both cores easily. Has [[Keyword/Breakdown]] to take advantage of having an injured core for more damage.",
Frontmatter = new()
{
{ "category", "Faction" },
},
},
new()
{
Title = "Sungrace",
Category = "Faction",
Content = "# Energy Reserves\r\n\r\nCare about always having banked energy. Spells can refund themselves when using [[Energy Reserves]], or get stronger is\r\nyou [[Overflow]] your energy reserve.\r\n\r\nConcepts:\r\n\r\n- Ramp\r\n- Using and Replenishing Reserved Energy\r\n-",
Frontmatter = new()
{
{ "category", "Faction" },
},
},
new()
{ {
Title = "Blitz", Title = "Blitz",
Category = "Keyword", Category = "Keyword",
@@ -841,10 +792,88 @@ public static class DocDatabase
}, },
}, },
new() new()
{
Title = "Lifeblood",
Category = "Syndicate",
Content = "# Timelines\n\n## Deadly Fauna\n\nGives everything +1 / +1.\n\n## Abundant Growth\n\nGives everything [[Overpower]].\n\n# Mechanics\n\n## Flourish\n\nFlourish.\n## Combat\n\nCombat.\n## Evasive\n\nEvasive.\n\n## Sprout\n\nSprout.\n\n# Wolves\n\nWolves.\n\n# Key Cards\n\n## Defensive Cards:\n- [[Bloom]]\n- [[Mulch]]\n- [[Focused Adaptation]]\n- [[Symbiosis]]\n\n\n## Threats\n- [[Glade Grazers]]\n- [[Sapling Dryad]]\n- [[Spark of Bounty]]\n- [[Glasswinged Monarch]]\n- [[Zealot of the Hunt]]\n- [[Hungry Tyrannosaur]]",
Frontmatter = new()
{
{ "category", "Syndicate" },
},
Syndicates = [
"Lifeblood",
],
},
new()
{
Title = "Phasetide",
Category = "Syndicate",
Content = "# Timelines\n\n## The One True Timeline\n\nHeals everything 1 health each turn.\n\nA lot of Phasetide agents simply immortalize in this timeline. And a lot of Phasetide agents start damage to take advantage of this healing effect.\n\nAnd some Phasetide agents will hate it when your not in the timeline. Such as [[Shattersmith]], which will flat out die if too many timelines exist in the stack that are not The One True timeline.\n\n# Mechanics\n\n## Healing\n\nHealing.\n\nCards:\n- [[Bearer of the Broth]]\n\nPayoff:\n- [[Bareknuckle Inquisitor]]\n\n## Rewind\n\nRewind.\n\nCards: \n- [[Temple Analyst]]\n\nPayoff:\n- [[Divergence Assassin]]\n\n## Phase\n\nPhase.\n\nCards:\n- [[Set in Stone]]\n\nPayoff:\n- [[Karmic Debtor]]\n\n## Reduce Cost\n\nReduce cost.\n\nCards:\n- [[Slow Convert]]\n\nPayoff:\n- [[Stern Arbiter]]\n\n# Key Cards\n\n## Defensive Cards:\n- [[Prayer of Rescue]]\n- [[Invigorating Balm]]\n- [[Set in Stone]]\n- [[Out of Line]]\n\n## Threats\n- [[Temple Analyst]]\n- [[Consummate Conspirator]]\n- [[Bareknuckle Inquisitor]]\n- [[Divergence Assassin]]\n- [[Stern Arbiter]]",
Frontmatter = new()
{
{ "category", "Syndicate" },
},
Syndicates = [
"Phasetide",
],
},
new()
{
Title = "Silence",
Category = "Syndicate",
Content = "# Timelines\n\n## Voiceless Skies\n\nCauses it so that if you have one agent in play, that agent gets +3 / +3.\nWith deplete, you can ensure the player ideally can never properly take advantage of the timeline.\n\nIt turn on some of your key board survival cards, like [[Braindead Bouncer]] and [[Nameless Spirit]].\n\nIt also notably interacts with [[Phase]]. So you can phase your [[Sensory Deprivation Pod]] to potentially activate Voiceless Skies for your other blocker, or use [[Bury the Evidence]] if you have two 1 / 1 cards and need to remove one. \n\n# Mechanics\n\n## Deplete\n\nPrevents a card from attacking and blocking if they have not already attacked or blocked. Also prevents a card from being activated, although outside of [[Mind Over Matter]] during [[Voiceless Sky]], the opposing agent can always activate in a response.\n\nCards:\n- [[Return to Stillness]]\n\nPayoff:\n- [[Telepathic Scavenger]]\n\n## Mute\n\nCan remove all keywords and effects on an agent. Note the order matters. If you mute a target, and an effect gets added on after, the agent will have the newly added effect while still being considered muted.\n\nCards: \n- [[Muffle]]\n\nPayoff:\n- [[Destiny Ripper]]\n\n## Revival\n\nCan bring cards back from the dead. Currently mostly a mechanic in practice for keeping [[Somnus, the Dreaming]] alive.\n\nCards:\n- [[The Forgotten Tale]]\n\nPayoff:\n- [[Solemn Attendant]]\n\n## Spell Duplication\n\nHas access to duplicating spells.\n\nCards:\n- [[Disciplined Student]]\n\nPayoff:\n- [[Overburdened Scribe]]\n\n## Activations\n\nCards with strong activation effects. And it's revival package also allows you to kill off and revive your own cards to activate them again in a fresh state.\n\nCards:\n- [[Death Jockey]]\n\nPayoff:\n- [[Rotting Rocker]]\n\n# Key Cards\n\n## Defensive Cards:\n- [[Hush Now]]\n- [[Spread the Sickness]]\n- [[Pressure Spike]]\n- [[Muffle]]\n\n## Threats\n- [[Rotting Rocker]]\n- [[Telepathic Scavenger]]\n- [[Sensory Deprivation Pod]]\n- [[Overburdened Scribe]]\n- [[The Forgotten Tale]]",
Frontmatter = new()
{
{ "category", "Syndicate" },
},
Syndicates = [
"Silence",
],
},
new()
{
Title = "Singularity",
Category = "Syndicate",
Content = "# Timelines\n\n## Erudite Beacon\n\nCauses you to draw a card when you shift to it, and a card every round. You will have many cards in hand, so if you play in this timeline, you need ways to spend your hand to avoid erasing cards off the top of your deck. \n\n[[Enlightened Refugee]] is the simplest way to shift to this [[Timeline]].\n\n# Mechanics\n\n## Discard\n\nHas a lot of cards that can be removed from hand for added benefits, and a lot of cards that can discard for additional card draw.\n\nCards:\n- [[Efficient Scrapbot]]\n\nPayoff:\n- [[Rapid Iteration]]\n\n## Disarm\n\nCan reduce agents attack values to zero. Way to reduce damage for a turn or win combat trades.\n\nCards:\n- [[Frontline Fellowship]]\n\nPayoff:\n- [[Traffic Conductor]]\n\n## Agent Duplication\n\nHas access to duplicating agents.\n\nCards:\n- [[Chronal Scan]]\n\nPayoff:\n- [[Springloaded Hopper]]\n\n## Pocket Scout\n\n1 / 1 token that can swarm the board or chump block.\n\nCards:\n- [[Enthusiastic Bot-Poke]]\n\nPayoff:\n- [[Scattered Helpers]]\n\n# Key Cards\n\n## Defensive Cards:\n- [[Timestop]]\n- [[Rapid Iteration]]\n- [[Frontline Fellowship]]\n\n## Threats\n- [[Roiling Amalgam]]\n- [[Rogue Amalgam]]\n- [[Quicksilver Khaela]]\n- [[Scuttling Spares]]\n- [[Springloaded Hopper]]",
Frontmatter = new()
{
{ "category", "Syndicate" },
},
Syndicates = [
"Singularity",
],
},
new()
{
Title = "Splintergleam",
Category = "Syndicate",
Content = "# Timelines\n\n## Volcanic Rivers\n\nDeals 1 damage to each core when shifted to.\n\n## Torment\n\n# Mechanics\n\n## Bleed\n\nBleed.\n## Big Timeline Stack\n\nBig Timeline Stack\n## Fervor\n\nFervor\n\n## Self Wound\n\nSelf Wound\n\n\n# Key Cards\n\n## Defensive Cards:\n- [[Soothing Glow]]\n- [[Chaos Control]]\n- [[Radiant Channeling]]\n- [[Supernova]]\n- [[Throw into the Sun]]\n- [[Entropy's End]]\n\n\n## Threats\n- [[Canine Adjutant]]\n- [[Limit Breaker]]\n- [[Debris Collector]]\n- [[Jury of the Second Law]]\n- [[Lightsteel Colossus]]\n- [[APEX Starcruise]]\n- [[Devourer Spawn]]",
Frontmatter = new()
{
{ "category", "Syndicate" },
},
Syndicates = [
"Splintergleam",
],
},
new()
{
Title = "Sungrace",
Category = "Syndicate",
Content = "# Timelines\n\n## Star Siphon\n\nRefill your energy reserves every turn. Not, this will not overflow your energy reserve if your energy reserve is full and you otherwise have no other energy. Now if you have 1 energy, and 0 reserves, even though you have 2 less mana, it will overflow. So your encouraged to spend all your reserves and always leave one normal mana left over a turn to trigger your overflows.\n\n# Mechanics\n\n## Overflow\n\nEnergy Reserves.\n## Energy Reserves\n\nEnergy Reserves.\n## Spend 12+ Energy\n\nSpend 12+ Energy.\n\n## Ramp\n\nRamp.\n\n# Spells\n\nSpells.\n\n## Blitz\n\nBlitz.\n\n# Key Cards\n\n## Defensive Cards:\n- [[Soothing Glow]]\n- [[Chaos Control]]\n- [[Radiant Channeling]]\n- [[Supernova]]\n- [[Throw into the Sun]]\n- [[Entropy's End]]\n\n\n## Threats\n- [[Canine Adjutant]]\n- [[Limit Breaker]]\n- [[Debris Collector]]\n- [[Jury of the Second Law]]\n- [[Lightsteel Colossus]]\n- [[APEX Starcruise]]\n- [[Devourer Spawn]]",
Frontmatter = new()
{
{ "category", "Syndicate" },
},
Syndicates = [
"Sungrace",
],
},
new()
{ {
Title = "Lifeblood - Phasetide", Title = "Lifeblood - Phasetide",
Category = "Syndicate Pairings", 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.", Content = "Pairing has cards that care about [[Shift]] like [[Heretic Whistleblower]] and [[Convergent Pack]] so you could go that route.\r\n\r\nBoth pairings also have access to [[Flourish]], so you could go a board route and use [[Tidal Wave]] as a finisher.",
Frontmatter = new() Frontmatter = new()
{ {
{ "syndicates", "- Lifeblood\n- Phasetide" }, { "syndicates", "- Lifeblood\n- Phasetide" },
@@ -860,7 +889,7 @@ public static class DocDatabase
{ {
Title = "Lifeblood - Silence", Title = "Lifeblood - Silence",
Category = "Syndicate Pairings", 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.", Content = "Typically the [[Somnus, the Dreaming]] pairing. Access to strong cards like [[Sap Sapper]] and [[Sleepy Druid]].\r\n\r\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() Frontmatter = new()
{ {
{ "syndicates", "- Lifeblood\n- Silence" }, { "syndicates", "- Lifeblood\n- Silence" },
@@ -875,7 +904,7 @@ public static class DocDatabase
{ {
Title = "Lifeblood - Singularity", Title = "Lifeblood - Singularity",
Category = "Syndicate Pairings", 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.", Content = "For this pairing, I typically go [[Sprout]]. \r\n\r\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() Frontmatter = new()
{ {
{ "syndicates", "- Lifeblood\n- Singularity" }, { "syndicates", "- Lifeblood\n- Singularity" },
@@ -906,7 +935,7 @@ public static class DocDatabase
{ {
Title = "Lifeblood - Sungrace", Title = "Lifeblood - Sungrace",
Category = "Syndicate Pairings", 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.", Content = "Both actually have access to [[Flourish]], given [[Limit Breaker]] and [[Soothing Glow]].\r\n\r\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() Frontmatter = new()
{ {
{ "syndicates", "- Lifeblood\n- Sungrace" }, { "syndicates", "- Lifeblood\n- Sungrace" },
@@ -921,7 +950,7 @@ public static class DocDatabase
{ {
Title = "Phasetide - Silence", Title = "Phasetide - Silence",
Category = "Syndicate Pairings", 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]].", Content = "I suppose a control heavily [[The One True Timeline]] deck. \r\n\r\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.\r\n\r\nAnd I suppose given how easy it is to immortalize your cards with the one true timeline, [[Solemn Attendant]] will always have a target.\r\n\r\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.\r\n\r\nSo mid range [[The One True Timeline]] deck with [[Silence]] utilities and removal.\r\n\r\nAnd potential [[The Firm Hand]] can be what turns on your [[Overburdened Scribe]].",
Frontmatter = new() Frontmatter = new()
{ {
{ "syndicates", "- Phasetide\n- Silence" }, { "syndicates", "- Phasetide\n- Silence" },
@@ -966,7 +995,7 @@ public static class DocDatabase
{ {
Title = "Phasetide - Sungrace", Title = "Phasetide - Sungrace",
Category = "Syndicate Pairings", 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.", Content = "You can [[Snap Back]] your [[Supernova]] for so much mana.\r\n\r\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() Frontmatter = new()
{ {
{ "syndicates", "- Phasetide\n- Sungrace" }, { "syndicates", "- Phasetide\n- Sungrace" },
@@ -982,7 +1011,7 @@ public static class DocDatabase
{ {
Title = "Silence - Singularity", Title = "Silence - Singularity",
Category = "Syndicate Pairings", 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.", Content = "I like going [[Deplete]]. Singularity has good removal and [[Timestop]]. \r\n\r\nIf your deplete plan still fails, you have [[Zorp, Unrecyclable]] to continue the attacking plan forever.",
Frontmatter = new() Frontmatter = new()
{ {
{ "syndicates", "- Silence\n- Singularity" }, { "syndicates", "- Silence\n- Singularity" },
@@ -1012,7 +1041,7 @@ public static class DocDatabase
{ {
Title = "Silence - Sungrace", Title = "Silence - Sungrace",
Category = "Syndicate Pairings", 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.", Content = "The typical control pairing.\r\n\r\nYou can put down one threat at a time, like [[Limit Breaker]], [[Canine Adjutant]], [[Debris Collector]] and [[Jury of the Second Law]].\r\n\r\nAll your [[Silence]] spells will kill their threats, plus you can [[Overflow]] for days.\r\n\r\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.\r\n\r\nIf you encounter a [[Devourer Spawn]] or [[Roiling Amalgam]] you can always [[Throw into the Sun]].\r\n\r\n[[APEX Starcruise]] is another top board wipe that also has a big body to start doing damage.",
Frontmatter = new() Frontmatter = new()
{ {
{ "syndicates", "- Silence\n- Sungrace" }, { "syndicates", "- Silence\n- Sungrace" },
@@ -1057,7 +1086,7 @@ public static class DocDatabase
{ {
Title = "Splintergleam - Sungrace", Title = "Splintergleam - Sungrace",
Category = "Syndicate Pairings", 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.", Content = "I saw someone using [[Lightsteel Engineer]] with the entire [[Volcanic Rivers]] package, and I just love it.\r\n\r\nTechnically, [[Gentle Frank]] also reduces core damage, but I feel like your going to die before 8 mana.\r\n\r\n[[Clarion, Deepest Breath]] turns your [[Volcanic Rivers]] shifts into life gain, but then you need to run the [[Overflow]] package.",
Frontmatter = new() Frontmatter = new()
{ {
{ "syndicates", "- Splintergleam\n- Sungrace" }, { "syndicates", "- Splintergleam\n- Sungrace" },
+195 -45
View File
@@ -1,4 +1,5 @@
@using System.Text.RegularExpressions @using System.Text.RegularExpressions
@using Microsoft.AspNetCore.Components.Rendering
@namespace Chrono.Components @namespace Chrono.Components
@if (Card != null) @if (Card != null)
@@ -50,7 +51,9 @@
{ {
<div class="detail-field description"> <div class="detail-field description">
<span class="field-label"><i class="bi bi-chat-quote-fill"></i></span> <span class="field-label"><i class="bi bi-chat-quote-fill"></i></span>
<span class="field-value">@RenderDescription(Card.Description)</span> <div class="markdown-content">
@RenderDescription(Card.Description)
</div>
</div> </div>
} }
@@ -90,7 +93,7 @@
if (target != null) if (target != null)
{ {
<button class="inline-card-btn" <button class="inline-card-btn"
@onclick="() => Navigate(target)">@targetName</button> @onclick="() => Navigate(targetName)">@targetName</button>
} }
else else
{ {
@@ -111,7 +114,7 @@
if (fromCard != null) if (fromCard != null)
{ {
<button class="inline-card-btn" <button class="inline-card-btn"
@onclick="() => Navigate(fromCard)">@Card.ImmortalizeFrom</button> @onclick="() => Navigate(Card.ImmortalizeFrom)">@Card.ImmortalizeFrom</button>
} }
else else
{ {
@@ -142,7 +145,7 @@
@code { @code {
[Parameter] public CardData? Card { get; set; } [Parameter] public CardData? Card { get; set; }
[Parameter] public EventCallback OnClose { get; set; } [Parameter] public EventCallback OnClose { get; set; }
[Parameter] public EventCallback<CardData> OnNavigate { get; set; } [Parameter] public EventCallback<string> OnNavigate { get; set; }
[Parameter] public bool ShowNotes { get; set; } [Parameter] public bool ShowNotes { get; set; }
[Inject] private HttpClient Http { get; set; } = default!; [Inject] private HttpClient Http { get; set; } = default!;
@@ -203,9 +206,19 @@
_ = OnClose.InvokeAsync(); _ = OnClose.InvokeAsync();
} }
private void Navigate(CardData card) private void Navigate(string name)
{ {
_ = OnNavigate.InvokeAsync(card); var card = LookupCard(name);
if (card != null)
{
Card = card;
_ = LoadNote();
StateHasChanged();
}
else
{
_ = OnNavigate.InvokeAsync(name);
}
} }
private static CardData? LookupCard(string cardName) private static CardData? LookupCard(string cardName)
@@ -214,68 +227,205 @@
string.Equals(c.Name, cardName, StringComparison.OrdinalIgnoreCase)); string.Equals(c.Name, cardName, StringComparison.OrdinalIgnoreCase));
} }
private RenderFragment RenderDescription(string description) private static DocData? LookupDoc(string title)
{
return DocDatabase.Docs.FirstOrDefault(d =>
string.Equals(d.Title, title, StringComparison.OrdinalIgnoreCase));
}
private RenderFragment RenderDescription(string content)
{ {
return builder => return builder =>
{ {
var cardNames = CardDatabase.Cards if (string.IsNullOrWhiteSpace(content)) return;
.Select(c => c.Name)
.Where(n => !string.IsNullOrWhiteSpace(n))
.OrderByDescending(n => n.Length)
.ToList();
var seq = 0; var seq = 0;
var remaining = description; var lines = content.Split(['\r', '\n'], StringSplitOptions.None);
var inList = false;
var tableLines = new List<string>();
var inTable = false;
while (!string.IsNullOrEmpty(remaining)) for (var i = 0; i < lines.Length; i++)
{ {
var bestIdx = -1; var line = lines[i];
string? bestName = null; var trimmedLine = line.Trim();
foreach (var name in cardNames) // Handle Tables
if (trimmedLine.StartsWith("|") && trimmedLine.EndsWith("|"))
{ {
var pattern = $@"\b{Regex.Escape(name)}\b"; inTable = true;
var match = Regex.Match(remaining, pattern, RegexOptions.IgnoreCase); tableLines.Add(trimmedLine);
continue;
if (match.Success) }
{ else if (inTable)
if (bestIdx == -1 || match.Index < bestIdx) {
{ RenderTable(builder, ref seq, tableLines);
bestIdx = match.Index; tableLines.Clear();
bestName = match.Value; inTable = false;
}
}
} }
if (bestIdx != -1 && bestName != null) // Handle Lists
if (trimmedLine.StartsWith("- ") || trimmedLine.StartsWith("* "))
{ {
if (bestIdx > 0) if (!inList)
builder.AddContent(seq++, remaining[..bestIdx]); {
builder.OpenElement(seq++, "ul");
inList = true;
}
var isCardListItem = trimmedLine.Contains("[[") && trimmedLine.Contains("]]");
builder.OpenElement(seq++, "li");
RenderInlines(builder, ref seq, trimmedLine[2..], true);
builder.CloseElement();
continue;
}
else if (inList)
{
builder.CloseElement(); // Close ul
inList = false;
}
var card = LookupCard(bestName); // Handle Headings
if (card != null) if (trimmedLine.StartsWith("### "))
{
builder.OpenElement(seq++, "h4");
RenderInlines(builder, ref seq, trimmedLine[4..], false);
builder.CloseElement();
}
else if (trimmedLine.StartsWith("## "))
{
builder.OpenElement(seq++, "h3");
RenderInlines(builder, ref seq, trimmedLine[3..], false);
builder.CloseElement();
}
else if (trimmedLine.StartsWith("# "))
{
builder.OpenElement(seq++, "h2");
RenderInlines(builder, ref seq, trimmedLine[2..], false);
builder.CloseElement();
}
else if (!string.IsNullOrWhiteSpace(trimmedLine))
{
builder.OpenElement(seq++, "p");
RenderInlines(builder, ref seq, line, false);
builder.CloseElement();
}
}
if (inTable) RenderTable(builder, ref seq, tableLines);
if (inList) builder.CloseElement();
};
}
private void RenderTable(RenderTreeBuilder builder, ref int seq, List<string> lines)
{
if (lines.Count < 2) return;
builder.OpenElement(seq++, "div");
builder.AddAttribute(seq++, "class", "table-responsive");
builder.OpenElement(seq++, "table");
builder.AddAttribute(seq++, "class", "markdown-table");
var hasHeader = lines.Count > 2 && lines[1].Contains("---");
var startIdx = 0;
if (hasHeader)
{
builder.OpenElement(seq++, "thead");
builder.OpenElement(seq++, "tr");
var cells = lines[0].Split('|', StringSplitOptions.RemoveEmptyEntries);
foreach (var cell in cells)
{
builder.OpenElement(seq++, "th");
RenderInlines(builder, ref seq, cell.Trim(), false);
builder.CloseElement();
}
builder.CloseElement();
builder.CloseElement();
startIdx = 2;
}
builder.OpenElement(seq++, "tbody");
for (var i = startIdx; i < lines.Count; i++)
{
if (i == 1 && hasHeader) continue;
builder.OpenElement(seq++, "tr");
var cells = lines[i].Split('|', StringSplitOptions.RemoveEmptyEntries);
foreach (var cell in cells)
{
builder.OpenElement(seq++, "td");
RenderInlines(builder, ref seq, cell.Trim(), false);
builder.CloseElement();
}
builder.CloseElement();
}
builder.CloseElement();
builder.CloseElement();
builder.CloseElement();
}
private void RenderInlines(RenderTreeBuilder builder, ref int seq, string text, bool renderAsImage)
{
var remaining = text;
while (!string.IsNullOrEmpty(remaining))
{
var boldMatch = Regex.Match(remaining, @"\*\*(.*?)\*\*");
var italicMatch = Regex.Match(remaining, @"\*(.*?)\*");
var cardMatch = Regex.Match(remaining, @"\[\[(.*?)\]\]");
var bestIdx = int.MaxValue;
Match? bestMatch = null;
int matchType = 0; // 1: bold, 2: italic, 3: card
if (boldMatch.Success && boldMatch.Index < bestIdx) { bestIdx = boldMatch.Index; bestMatch = boldMatch; matchType = 1; }
if (italicMatch.Success && italicMatch.Index < bestIdx) { bestIdx = italicMatch.Index; bestMatch = italicMatch; matchType = 2; }
if (cardMatch.Success && cardMatch.Index < bestIdx) { bestIdx = cardMatch.Index; bestMatch = cardMatch; matchType = 3; }
if (bestMatch != null)
{
if (bestIdx > 0)
builder.AddContent(seq++, remaining[..bestIdx]);
var innerText = bestMatch.Groups[1].Value;
if (matchType == 1) // Bold
{
builder.OpenElement(seq++, "strong");
builder.AddContent(seq++, innerText);
builder.CloseElement();
}
else if (matchType == 2) // Italic
{
builder.OpenElement(seq++, "em");
builder.AddContent(seq++, innerText);
builder.CloseElement();
}
else if (matchType == 3) // Card Link / Doc Link
{
var card = LookupCard(innerText);
var doc = LookupDoc(innerText);
if (card != null || doc != null)
{ {
builder.OpenElement(seq++, "button"); builder.OpenElement(seq++, "button");
builder.AddAttribute(seq++, "class", "inline-card-btn"); builder.AddAttribute(seq++, "class", "inline-card-btn");
builder.AddAttribute(seq++, "onclick", builder.AddAttribute(seq++, "onclick", EventCallback.Factory.Create(this, () => Navigate(innerText)));
EventCallback.Factory.Create(this, () => Navigate(card))); builder.AddContent(seq++, innerText);
builder.AddContent(seq++, bestName);
builder.CloseElement(); builder.CloseElement();
} }
else else
{ {
builder.AddContent(seq++, bestName); builder.AddContent(seq++, innerText);
} }
}
remaining = remaining[(bestIdx + bestName.Length)..]; remaining = remaining[(bestIdx + bestMatch.Length)..];
}
else
{
builder.AddContent(seq++, remaining);
remaining = "";
}
} }
}; else
{
builder.AddContent(seq++, remaining);
remaining = "";
}
}
} }
} }
@@ -1,7 +1,7 @@
.modal-backdrop { .modal-backdrop {
position: fixed; position: fixed;
inset: 0; inset: 0;
z-index: 1040; z-index: 1060;
background: rgba(0, 0, 0, 0.7); background: rgba(0, 0, 0, 0.7);
backdrop-filter: blur(4px); backdrop-filter: blur(4px);
animation: fade-in 0.2s ease-out; animation: fade-in 0.2s ease-out;
@@ -21,7 +21,7 @@
top: 50%; top: 50%;
left: 50%; left: 50%;
transform: translate(-50%, -50%); transform: translate(-50%, -50%);
z-index: 1050; z-index: 1070;
background: var(--bg-surface); background: var(--bg-surface);
border: 1px solid var(--border); border: 1px solid var(--border);
border-radius: 16px; border-radius: 16px;
@@ -200,8 +200,55 @@
color: var(--text-secondary); color: var(--text-secondary);
} }
.detail-field.description .field-value { .markdown-content {
color: var(--text-primary); color: var(--text-primary);
line-height: 1.6;
}
.markdown-content p {
margin-bottom: 0.75rem;
}
.markdown-content ul {
margin-bottom: 0.75rem;
padding-left: 1.25rem;
}
.markdown-content li {
margin-bottom: 0.25rem;
}
.markdown-content h2, .markdown-content h3, .markdown-content h4 {
margin-top: 1rem;
margin-bottom: 0.75rem;
font-weight: 700;
}
.markdown-content h2 { font-size: 1.3rem; }
.markdown-content h3 { font-size: 1.2rem; }
.markdown-content h4 { font-size: 1rem; }
.table-responsive {
overflow-x: auto;
margin-bottom: 1rem;
border: 1px solid var(--border);
border-radius: 8px;
}
.markdown-table {
width: 100%;
border-collapse: collapse;
font-size: 0.85rem;
}
.markdown-table th, .markdown-table td {
padding: 0.5rem;
border: 1px solid var(--border);
text-align: left;
}
.markdown-table th {
background: rgba(255, 255, 255, 0.05);
} }
.card-detail::-webkit-scrollbar { .card-detail::-webkit-scrollbar {
@@ -0,0 +1,118 @@
@using Model
@using System.Text.RegularExpressions
@using Microsoft.AspNetCore.Components.Rendering
@namespace Chrono.Components
@if (Doc != null)
{
<div class="modal-backdrop" @onclick="HandleBackdropClick"></div>
<div class="generic-detail">
<button class="detail-close" @onclick="HandleClose"><i class="bi bi-x-lg"></i></button>
<div class="detail-layout">
<div class="detail-info">
<div class="detail-header">
@if (!string.IsNullOrEmpty(Doc.Category))
{
<span class="category-badge">@Doc.Category</span>
}
<h2>@Doc.Title</h2>
</div>
@if (!string.IsNullOrWhiteSpace(Doc.Content))
{
<div class="detail-field description">
<div class="markdown-content">
@RenderDescription(Doc.Content)
</div>
</div>
}
</div>
</div>
</div>
}
@code {
[Parameter] public DocData? Doc { get; set; }
[Parameter] public EventCallback OnClose { get; set; }
private void HandleClose() => _ = OnClose.InvokeAsync();
private void HandleBackdropClick() => _ = OnClose.InvokeAsync();
private RenderFragment RenderDescription(string content)
{
return builder =>
{
if (string.IsNullOrWhiteSpace(content)) return;
var seq = 0;
var lines = content.Split(['\r', '\n'], StringSplitOptions.None);
var inList = false;
for (var i = 0; i < lines.Length; i++)
{
var line = lines[i];
var trimmedLine = line.Trim();
// Handle Lists
if (trimmedLine.StartsWith("- ") || trimmedLine.StartsWith("* "))
{
if (!inList)
{
builder.OpenElement(seq++, "ul");
inList = true;
}
builder.OpenElement(seq++, "li");
RenderInlines(builder, ref seq, trimmedLine[2..]);
builder.CloseElement();
continue;
}
else if (inList)
{
builder.CloseElement();
inList = false;
}
if (!string.IsNullOrWhiteSpace(trimmedLine))
{
builder.OpenElement(seq++, "p");
RenderInlines(builder, ref seq, line);
builder.CloseElement();
}
}
if (inList) builder.CloseElement();
};
}
private void RenderInlines(RenderTreeBuilder builder, ref int seq, string text)
{
var remaining = text;
while (!string.IsNullOrEmpty(remaining))
{
var boldMatch = Regex.Match(remaining, @"\*\*(.*?)\*\*");
var italicMatch = Regex.Match(remaining, @"\*(.*?)\*");
var bestIdx = int.MaxValue;
Match? bestMatch = null;
int matchType = 0; // 1: bold, 2: italic
if (boldMatch.Success && boldMatch.Index < bestIdx) { bestIdx = boldMatch.Index; bestMatch = boldMatch; matchType = 1; }
if (italicMatch.Success && italicMatch.Index < bestIdx) { bestIdx = italicMatch.Index; bestMatch = italicMatch; matchType = 2; }
if (bestMatch != null)
{
if (bestIdx > 0) builder.AddContent(seq++, remaining[..bestIdx]);
var innerText = bestMatch.Groups[1].Value;
builder.OpenElement(seq++, matchType == 1 ? "strong" : "em");
builder.AddContent(seq++, innerText);
builder.CloseElement();
remaining = remaining[(bestIdx + bestMatch.Length)..];
}
else
{
builder.AddContent(seq++, remaining);
remaining = "";
}
}
}
}
@@ -0,0 +1,76 @@
.modal-backdrop {
position: fixed;
inset: 0;
z-index: 1060;
background: rgba(0, 0, 0, 0.7);
backdrop-filter: blur(4px);
}
.generic-detail {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
z-index: 1070;
background: var(--bg-surface);
border: 1px solid var(--border);
border-radius: 20px;
padding: 2rem;
max-width: 600px;
width: 90vw;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.6);
color: var(--text-primary);
}
.detail-close {
position: absolute;
top: 1rem;
right: 1rem;
background: rgba(0, 0, 0, 0.3);
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;
transition: all 0.2s;
}
.detail-close:hover {
background: rgba(255, 255, 255, 0.1);
transform: rotate(90deg);
}
.detail-header {
text-align: center;
margin-bottom: 1.5rem;
}
.category-badge {
display: inline-block;
padding: 0.2rem 0.6rem;
background: var(--accent);
color: white;
font-size: 0.75rem;
font-weight: 700;
border-radius: 4px;
margin-bottom: 0.5rem;
text-transform: uppercase;
}
.detail-header h2 {
margin: 0;
font-size: 1.5rem;
font-weight: 800;
}
.markdown-content {
line-height: 1.6;
}
.markdown-content p {
margin-bottom: 1rem;
}
@@ -36,6 +36,11 @@
<i class="bi bi-diagram-2-fill nav-icon"></i> Syndicates <i class="bi bi-diagram-2-fill nav-icon"></i> Syndicates
</NavLink> </NavLink>
</div> </div>
<div class="nav-item px-3">
<NavLink class="nav-link" href="syndicate-pairings">
<i class="bi bi-intersect nav-icon"></i> Pairings
</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,258 @@
@using System.Text.RegularExpressions
@using Microsoft.AspNetCore.Components.Rendering
@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">
<span class="syn-icon @Doc.Title.ToLowerInvariant()" title="@Doc.Title"></span>
</div>
<h2>@Doc.Title</h2>
</div>
@if (!string.IsNullOrWhiteSpace(Doc.Content))
{
<div class="detail-field description">
<div class="markdown-content">
@RenderDescription(Doc.Content)
</div>
</div>
}
</div>
</div>
</div>
}
@code {
[Parameter] public DocData? Doc { get; set; }
[Parameter] public EventCallback OnClose { get; set; }
[Parameter] public EventCallback<string> OnNavigate { get; set; }
private void HandleClose()
{
_ = OnClose.InvokeAsync();
}
private void HandleBackdropClick()
{
_ = OnClose.InvokeAsync();
}
private void Navigate(string name)
{
_ = OnNavigate.InvokeAsync(name);
}
private static CardData? LookupCard(string cardName)
{
return CardDatabase.Cards.FirstOrDefault(c =>
string.Equals(c.Name, cardName, StringComparison.OrdinalIgnoreCase));
}
private static DocData? LookupDoc(string title)
{
return DocDatabase.Docs.FirstOrDefault(d =>
string.Equals(d.Title, title, StringComparison.OrdinalIgnoreCase));
}
private RenderFragment RenderDescription(string content)
{
return builder =>
{
if (string.IsNullOrWhiteSpace(content)) return;
var seq = 0;
var lines = content.Split(['\r', '\n'], StringSplitOptions.None);
var inList = false;
var tableLines = new List<string>();
var inTable = false;
for (var i = 0; i < lines.Length; i++)
{
var line = lines[i];
var trimmedLine = line.Trim();
// Handle Tables
if (trimmedLine.StartsWith("|") && trimmedLine.EndsWith("|"))
{
inTable = true;
tableLines.Add(trimmedLine);
continue;
}
else if (inTable)
{
RenderTable(builder, ref seq, tableLines);
tableLines.Clear();
inTable = false;
}
// Handle Lists
if (trimmedLine.StartsWith("- ") || trimmedLine.StartsWith("* "))
{
if (!inList)
{
builder.OpenElement(seq++, "ul");
inList = true;
}
var isCardListItem = trimmedLine.Contains("[[") && trimmedLine.Contains("]]");
builder.OpenElement(seq++, "li");
RenderInlines(builder, ref seq, trimmedLine[2..], true);
builder.CloseElement();
continue;
}
else if (inList)
{
builder.CloseElement(); // Close ul
inList = false;
}
// Handle Headings
if (trimmedLine.StartsWith("### "))
{
builder.OpenElement(seq++, "h4");
RenderInlines(builder, ref seq, trimmedLine[4..], false);
builder.CloseElement();
}
else if (trimmedLine.StartsWith("## "))
{
builder.OpenElement(seq++, "h3");
RenderInlines(builder, ref seq, trimmedLine[3..], false);
builder.CloseElement();
}
else if (trimmedLine.StartsWith("# "))
{
builder.OpenElement(seq++, "h2");
RenderInlines(builder, ref seq, trimmedLine[2..], false);
builder.CloseElement();
}
else if (!string.IsNullOrWhiteSpace(trimmedLine))
{
builder.OpenElement(seq++, "p");
RenderInlines(builder, ref seq, line, false);
builder.CloseElement();
}
}
if (inTable) RenderTable(builder, ref seq, tableLines);
if (inList) builder.CloseElement();
};
}
private void RenderTable(RenderTreeBuilder builder, ref int seq, List<string> lines)
{
if (lines.Count < 2) return;
builder.OpenElement(seq++, "div");
builder.AddAttribute(seq++, "class", "table-responsive");
builder.OpenElement(seq++, "table");
builder.AddAttribute(seq++, "class", "markdown-table");
var hasHeader = lines.Count > 2 && lines[1].Contains("---");
var startIdx = 0;
if (hasHeader)
{
builder.OpenElement(seq++, "thead");
builder.OpenElement(seq++, "tr");
var cells = lines[0].Split('|', StringSplitOptions.RemoveEmptyEntries);
foreach (var cell in cells)
{
builder.OpenElement(seq++, "th");
RenderInlines(builder, ref seq, cell.Trim(), false);
builder.CloseElement();
}
builder.CloseElement();
builder.CloseElement();
startIdx = 2;
}
builder.OpenElement(seq++, "tbody");
for (var i = startIdx; i < lines.Count; i++)
{
if (i == 1 && hasHeader) continue;
builder.OpenElement(seq++, "tr");
var cells = lines[i].Split('|', StringSplitOptions.RemoveEmptyEntries);
foreach (var cell in cells)
{
builder.OpenElement(seq++, "td");
RenderInlines(builder, ref seq, cell.Trim(), false);
builder.CloseElement();
}
builder.CloseElement();
}
builder.CloseElement();
builder.CloseElement();
builder.CloseElement();
}
private void RenderInlines(RenderTreeBuilder builder, ref int seq, string text, bool renderAsImage)
{
var remaining = text;
while (!string.IsNullOrEmpty(remaining))
{
var boldMatch = Regex.Match(remaining, @"\*\*(.*?)\*\*");
var italicMatch = Regex.Match(remaining, @"\*(.*?)\*");
var cardMatch = Regex.Match(remaining, @"\[\[(.*?)\]\]");
var bestIdx = int.MaxValue;
Match? bestMatch = null;
int matchType = 0; // 1: bold, 2: italic, 3: card
if (boldMatch.Success && boldMatch.Index < bestIdx) { bestIdx = boldMatch.Index; bestMatch = boldMatch; matchType = 1; }
if (italicMatch.Success && italicMatch.Index < bestIdx) { bestIdx = italicMatch.Index; bestMatch = italicMatch; matchType = 2; }
if (cardMatch.Success && cardMatch.Index < bestIdx) { bestIdx = cardMatch.Index; bestMatch = cardMatch; matchType = 3; }
if (bestMatch != null)
{
if (bestIdx > 0)
builder.AddContent(seq++, remaining[..bestIdx]);
var innerText = bestMatch.Groups[1].Value;
if (matchType == 1) // Bold
{
builder.OpenElement(seq++, "strong");
builder.AddContent(seq++, innerText);
builder.CloseElement();
}
else if (matchType == 2) // Italic
{
builder.OpenElement(seq++, "em");
builder.AddContent(seq++, innerText);
builder.CloseElement();
}
else if (matchType == 3) // Card Link / Doc Link
{
var card = LookupCard(innerText);
var doc = LookupDoc(innerText);
if (card != null || doc != null)
{
builder.OpenElement(seq++, "button");
builder.AddAttribute(seq++, "class", "inline-card-btn");
builder.AddAttribute(seq++, "onclick", EventCallback.Factory.Create(this, () => Navigate(innerText)));
builder.AddContent(seq++, innerText);
builder.CloseElement();
}
else
{
builder.AddContent(seq++, innerText);
}
}
remaining = remaining[(bestIdx + bestMatch.Length)..];
}
else
{
builder.AddContent(seq++, remaining);
remaining = "";
}
}
}
}
@@ -0,0 +1,200 @@
.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-field {
margin-bottom: 1.5rem;
}
.detail-field.description {
padding: 0;
border-radius: 0;
border: none;
}
.detail-field.description .field-value {
color: var(--text-primary);
display: block;
font-size: 1.1rem;
}
.markdown-content {
color: var(--text-primary);
line-height: 1.6;
}
.markdown-content p {
margin-bottom: 1rem;
}
.markdown-content ul {
margin-bottom: 1rem;
padding-left: 1.5rem;
}
.markdown-content li {
margin-bottom: 0.5rem;
}
.markdown-content h2, .markdown-content h3, .markdown-content h4 {
margin-top: 1.5rem;
margin-bottom: 1rem;
font-weight: 700;
}
.markdown-content h2 { font-size: 1.5rem; }
.markdown-content h3 { font-size: 1.3rem; }
.markdown-content h4 { font-size: 1.1rem; }
/* Syndicate Colors */
.lifeblood { color: #4caf50; }
.phasetide { color: #2196f3; }
.silence { color: #9c27b0; }
.singularity { color: #ffffff; }
.splintergleam { color: #f44336; }
.sungrace { color: #ff9800; }
.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;
}
}
@@ -1,4 +1,5 @@
@using System.Text.RegularExpressions @using System.Text.RegularExpressions
@using Microsoft.AspNetCore.Components.Rendering
@namespace Chrono.Components @namespace Chrono.Components
@if (Doc != null) @if (Doc != null)
@@ -24,7 +25,9 @@
@if (!string.IsNullOrWhiteSpace(Doc.Content)) @if (!string.IsNullOrWhiteSpace(Doc.Content))
{ {
<div class="detail-field description"> <div class="detail-field description">
<span class="field-value">@RenderDescription(Doc.Content)</span> <div class="markdown-content">
@RenderDescription(Doc.Content)
</div>
</div> </div>
} }
</div> </div>
@@ -35,7 +38,7 @@
@code { @code {
[Parameter] public DocData? Doc { get; set; } [Parameter] public DocData? Doc { get; set; }
[Parameter] public EventCallback OnClose { get; set; } [Parameter] public EventCallback OnClose { get; set; }
[Parameter] public EventCallback<CardData> OnNavigate { get; set; } [Parameter] public EventCallback<string> OnNavigate { get; set; }
private void HandleClose() private void HandleClose()
{ {
@@ -47,9 +50,9 @@
_ = OnClose.InvokeAsync(); _ = OnClose.InvokeAsync();
} }
private void Navigate(CardData card) private void Navigate(string name)
{ {
_ = OnNavigate.InvokeAsync(card); _ = OnNavigate.InvokeAsync(name);
} }
private static CardData? LookupCard(string cardName) private static CardData? LookupCard(string cardName)
@@ -58,82 +61,205 @@
string.Equals(c.Name, cardName, StringComparison.OrdinalIgnoreCase)); string.Equals(c.Name, cardName, StringComparison.OrdinalIgnoreCase));
} }
private RenderFragment RenderDescription(string description) private static DocData? LookupDoc(string title)
{
return DocDatabase.Docs.FirstOrDefault(d =>
string.Equals(d.Title, title, StringComparison.OrdinalIgnoreCase));
}
private RenderFragment RenderDescription(string content)
{ {
return builder => return builder =>
{ {
var cardNames = CardDatabase.Cards if (string.IsNullOrWhiteSpace(content)) return;
.Select(c => c.Name)
.Where(n => !string.IsNullOrWhiteSpace(n))
.OrderByDescending(n => n.Length)
.ToList();
var seq = 0; var seq = 0;
var remaining = description; var lines = content.Split(['\r', '\n'], StringSplitOptions.None);
var inList = false;
var tableLines = new List<string>();
var inTable = false;
while (!string.IsNullOrEmpty(remaining)) for (var i = 0; i < lines.Length; i++)
{ {
var bestIdx = -1; var line = lines[i];
string? bestName = null; var trimmedLine = line.Trim();
foreach (var name in cardNames) // Handle Tables
if (trimmedLine.StartsWith("|") && trimmedLine.EndsWith("|"))
{ {
var pattern = $@"\b{Regex.Escape(name)}\b"; inTable = true;
var match = Regex.Match(remaining, pattern, RegexOptions.IgnoreCase); tableLines.Add(trimmedLine);
continue;
if (match.Success) }
{ else if (inTable)
if (bestIdx == -1 || match.Index < bestIdx) {
{ RenderTable(builder, ref seq, tableLines);
bestIdx = match.Index; tableLines.Clear();
bestName = match.Value; inTable = false;
}
}
} }
// Also check for [[Card Name]] format which is common in docs // Handle Lists
var bracketMatch = Regex.Match(remaining, @"\[\[(.*?)\]\]"); if (trimmedLine.StartsWith("- ") || trimmedLine.StartsWith("* "))
if (bracketMatch.Success)
{ {
if (bestIdx == -1 || bracketMatch.Index < bestIdx) if (!inList)
{ {
bestIdx = bracketMatch.Index; builder.OpenElement(seq++, "ul");
bestName = bracketMatch.Value; inList = true;
} }
var isCardListItem = trimmedLine.Contains("[[") && trimmedLine.Contains("]]");
builder.OpenElement(seq++, "li");
RenderInlines(builder, ref seq, trimmedLine[2..], true);
builder.CloseElement();
continue;
}
else if (inList)
{
builder.CloseElement(); // Close ul
inList = false;
} }
if (bestIdx != -1 && bestName != null) // Handle Headings
if (trimmedLine.StartsWith("### "))
{ {
if (bestIdx > 0) builder.OpenElement(seq++, "h4");
builder.AddContent(seq++, remaining[..bestIdx]); RenderInlines(builder, ref seq, trimmedLine[4..], false);
builder.CloseElement();
}
else if (trimmedLine.StartsWith("## "))
{
builder.OpenElement(seq++, "h3");
RenderInlines(builder, ref seq, trimmedLine[3..], false);
builder.CloseElement();
}
else if (trimmedLine.StartsWith("# "))
{
builder.OpenElement(seq++, "h2");
RenderInlines(builder, ref seq, trimmedLine[2..], false);
builder.CloseElement();
}
else if (!string.IsNullOrWhiteSpace(trimmedLine))
{
builder.OpenElement(seq++, "p");
RenderInlines(builder, ref seq, line, false);
builder.CloseElement();
}
}
var cleanName = bestName.StartsWith("[[") && bestName.EndsWith("]]") if (inTable) RenderTable(builder, ref seq, tableLines);
? bestName.Substring(2, bestName.Length - 4) if (inList) builder.CloseElement();
: bestName; };
}
var card = LookupCard(cleanName);
if (card != null) private void RenderTable(RenderTreeBuilder builder, ref int seq, List<string> lines)
{
if (lines.Count < 2) return;
builder.OpenElement(seq++, "div");
builder.AddAttribute(seq++, "class", "table-responsive");
builder.OpenElement(seq++, "table");
builder.AddAttribute(seq++, "class", "markdown-table");
var hasHeader = lines.Count > 2 && lines[1].Contains("---");
var startIdx = 0;
if (hasHeader)
{
builder.OpenElement(seq++, "thead");
builder.OpenElement(seq++, "tr");
var cells = lines[0].Split('|', StringSplitOptions.RemoveEmptyEntries);
foreach (var cell in cells)
{
builder.OpenElement(seq++, "th");
RenderInlines(builder, ref seq, cell.Trim(), false);
builder.CloseElement();
}
builder.CloseElement();
builder.CloseElement();
startIdx = 2;
}
builder.OpenElement(seq++, "tbody");
for (var i = startIdx; i < lines.Count; i++)
{
if (i == 1 && hasHeader) continue;
builder.OpenElement(seq++, "tr");
var cells = lines[i].Split('|', StringSplitOptions.RemoveEmptyEntries);
foreach (var cell in cells)
{
builder.OpenElement(seq++, "td");
RenderInlines(builder, ref seq, cell.Trim(), false);
builder.CloseElement();
}
builder.CloseElement();
}
builder.CloseElement();
builder.CloseElement();
builder.CloseElement();
}
private void RenderInlines(RenderTreeBuilder builder, ref int seq, string text, bool renderAsImage)
{
// Simple regex-based inline parser for Bold, Italics, and [[Card Links]]
var remaining = text;
while (!string.IsNullOrEmpty(remaining))
{
var boldMatch = Regex.Match(remaining, @"\*\*(.*?)\*\*");
var italicMatch = Regex.Match(remaining, @"\*(.*?)\*");
var cardMatch = Regex.Match(remaining, @"\[\[(.*?)\]\]");
var bestIdx = int.MaxValue;
Match? bestMatch = null;
int matchType = 0; // 1: bold, 2: italic, 3: card
if (boldMatch.Success && boldMatch.Index < bestIdx) { bestIdx = boldMatch.Index; bestMatch = boldMatch; matchType = 1; }
if (italicMatch.Success && italicMatch.Index < bestIdx) { bestIdx = italicMatch.Index; bestMatch = italicMatch; matchType = 2; }
if (cardMatch.Success && cardMatch.Index < bestIdx) { bestIdx = cardMatch.Index; bestMatch = cardMatch; matchType = 3; }
if (bestMatch != null)
{
if (bestIdx > 0)
builder.AddContent(seq++, remaining[..bestIdx]);
var innerText = bestMatch.Groups[1].Value;
if (matchType == 1) // Bold
{
builder.OpenElement(seq++, "strong");
builder.AddContent(seq++, innerText);
builder.CloseElement();
}
else if (matchType == 2) // Italic
{
builder.OpenElement(seq++, "em");
builder.AddContent(seq++, innerText);
builder.CloseElement();
}
else if (matchType == 3) // Card Link / Doc Link
{
var card = LookupCard(innerText);
var doc = LookupDoc(innerText);
if (card != null || doc != null)
{ {
builder.OpenElement(seq++, "button"); builder.OpenElement(seq++, "button");
builder.AddAttribute(seq++, "class", "inline-card-btn"); builder.AddAttribute(seq++, "class", "inline-card-btn");
builder.AddAttribute(seq++, "onclick", builder.AddAttribute(seq++, "onclick", EventCallback.Factory.Create(this, () => Navigate(innerText)));
EventCallback.Factory.Create(this, () => Navigate(card))); builder.AddContent(seq++, innerText);
builder.AddContent(seq++, cleanName);
builder.CloseElement(); builder.CloseElement();
} }
else else
{ {
builder.AddContent(seq++, cleanName); builder.AddContent(seq++, innerText);
} }
}
remaining = remaining[(bestIdx + bestName.Length)..]; remaining = remaining[(bestIdx + bestMatch.Length)..];
}
else
{
builder.AddContent(seq++, remaining);
remaining = "";
}
} }
}; else
{
builder.AddContent(seq++, remaining);
remaining = "";
}
}
} }
} }
@@ -163,16 +163,43 @@
color: var(--text-primary); color: var(--text-primary);
display: block; display: block;
font-size: 1.1rem; font-size: 1.1rem;
white-space: pre-wrap;
} }
.markdown-content {
color: var(--text-primary);
line-height: 1.6;
}
.markdown-content p {
margin-bottom: 1rem;
}
.markdown-content ul {
margin-bottom: 1rem;
padding-left: 1.5rem;
}
.markdown-content li {
margin-bottom: 0.5rem;
}
.markdown-content h2, .markdown-content h3, .markdown-content h4 {
margin-top: 1.5rem;
margin-bottom: 1rem;
font-weight: 700;
}
.markdown-content h2 { font-size: 1.5rem; }
.markdown-content h3 { font-size: 1.3rem; }
.markdown-content h4 { font-size: 1.1rem; }
/* Syndicate Colors */ /* Syndicate Colors */
.lifeblood { color: #ef5350; } .lifeblood { color: #4caf50; }
.phasetide { color: #2196f3; } .phasetide { color: #2196f3; }
.silence { color: #9e9e9e; } .silence { color: #9c27b0; }
.singularity { color: #ffffff; } .singularity { color: #ffffff; }
.splintergleam { color: #4caf50; } .splintergleam { color: #f44336; }
.sungrace { color: #ffeb3b; } .sungrace { color: #ff9800; }
.inline-card-btn { .inline-card-btn {
background: none; background: none;
+15 -4
View File
@@ -136,7 +136,7 @@
{ {
<div class="card-cell @(selectedCard == card ? "selected" : "")" <div class="card-cell @(selectedCard == card ? "selected" : "")"
style="--i: @idx" style="--i: @idx"
@onclick="() => SelectCard(card)"> @onclick="() => SelectCard(card.Name)">
<div class="card-image-wrapper"> <div class="card-image-wrapper">
<div class="card-shimmer"></div> <div class="card-shimmer"></div>
<img src="@card.ImagePath" alt="@card.Name" loading="lazy" <img src="@card.ImagePath" alt="@card.Name" loading="lazy"
@@ -147,6 +147,13 @@
<div class="card-cost-badge" title="Cost">@card.Cost</div> <div class="card-cost-badge" title="Cost">@card.Cost</div>
} }
@if (card.IsSpell && !string.IsNullOrEmpty(card.Speed))
{
<div class="card-speed-badge" title="Speed">
<i class="bi bi-wind"></i> @card.Speed
</div>
}
@if (card.IsImmortalized) @if (card.IsImmortalized)
{ {
<div class="card-immortalize-badge" title="@(card.IsImmortalized ? "Immortalized" : "Immortalizes")"> <div class="card-immortalize-badge" title="@(card.IsImmortalized ? "Immortalized" : "Immortalizes")">
@@ -389,10 +396,14 @@
search = ""; search = "";
} }
private void SelectCard(CardData card) private void SelectCard(string name)
{ {
selectedCard = card; var card = allCards.FirstOrDefault(c => string.Equals(c.Name, name, StringComparison.OrdinalIgnoreCase));
_ = AnalyticsService.TrackEvent("select_item", new { item_name = card.Name, item_category = card.Category }); if (card != null)
{
selectedCard = card;
_ = AnalyticsService.TrackEvent("select_item", new { item_name = card.Name, item_category = card.Category });
}
} }
private void CloseDetail() private void CloseDetail()
+23
View File
@@ -645,6 +645,29 @@
border: 1px solid rgba(255, 215, 0, 0.3); border: 1px solid rgba(255, 215, 0, 0.3);
} }
.card-speed-badge {
position: absolute;
top: 0.4rem;
right: 0.4rem;
background: rgba(0, 0, 0, 0.75);
backdrop-filter: blur(4px);
color: #ce93d8; /* Match spell category color */
font-weight: 700;
font-size: 0.65rem;
padding: 0.2rem 0.5rem;
border-radius: 4px;
border: 1px solid rgba(206, 147, 216, 0.3);
z-index: 2;
line-height: 1;
display: flex;
align-items: center;
gap: 0.2rem;
}
.card-speed-badge i {
font-size: 0.75rem;
}
/* ── Card Label ── */ /* ── Card Label ── */
.card-label { .card-label {
display: flex; display: flex;
@@ -0,0 +1,105 @@
@page "/syndicate-pairings"
@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 Pairing</span>
</div>
</div>
}
</div>
</div>
<SyndicatePairDialog Doc="selectedDoc" OnClose="ClosePairing" OnNavigate="HandleNavigate" />
<CardDialog Card="selectedCard" OnClose="CloseCard" OnNavigate="HandleNavigate" />
<GenericDocDialog Doc="selectedGenericDoc" OnClose="CloseGenericDoc" />
@code {
private List<DocData> pairings = [];
private DocData? selectedDoc;
private CardData? selectedCard;
private DocData? selectedGenericDoc;
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;
}
private void CloseCard()
{
selectedCard = null;
}
private void CloseGenericDoc()
{
selectedGenericDoc = null;
}
private void HandleNavigate(string name)
{
var card = CardDatabase.Cards.FirstOrDefault(c => string.Equals(c.Name, name, StringComparison.OrdinalIgnoreCase));
if (card != null)
{
if (card.Category is "Agent" or "Spell")
{
selectedCard = card;
}
else
{
selectedGenericDoc = new DocData { Title = card.Name, Content = card.Description ?? "", Category = card.Category };
}
StateHasChanged();
return;
}
var doc = DocDatabase.Docs.FirstOrDefault(d => string.Equals(d.Title, name, StringComparison.OrdinalIgnoreCase));
if (doc != null)
{
selectedGenericDoc = doc;
StateHasChanged();
}
}
}
@@ -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; }
}
+55 -24
View File
@@ -1,66 +1,97 @@
@page "/syndicates" @page "/syndicates"
@using Model @using Model
@using Chrono.Components @using Chrono.Components
@inject NavigationManager Navigation @inject NavigationManager Navigation
<HeadContent> <HeadContent>
<meta name="description" content="Explore all syndicate pairings in Chrono CCG."/> <meta name="description" content="Explore each syndicate in Chrono CCG."/>
</HeadContent> </HeadContent>
<div class="syndicates-container"> <div class="syndicates-container">
<div class="syndicates-header"> <div class="syndicates-header">
<h1>Syndicate Pairings</h1> <h1>Syndicates</h1>
<p class="text-secondary">Explore all @pairings.Count combinations of syndicates</p> <p class="text-secondary">Explore the @syndicates.Count core syndicates of Chrono CCG</p>
</div> </div>
<div class="pairings-grid"> <div class="pairings-grid">
@foreach (var doc in pairings) @foreach (var doc in syndicates)
{ {
var s1 = doc.Syndicates?[0] ?? ""; var s = doc.Title;
var s2 = doc.Syndicates?[1] ?? ""; <div class="pairing-card" @onclick="() => OpenSyndicate(doc)">
<div class="pairing-card" @onclick="() => OpenPairing(doc)">
<div class="pairing-display"> <div class="pairing-display">
<div class="syndicate-item @s1.ToLowerInvariant()"> <div class="syndicate-item @s.ToLowerInvariant()">
<span class="syn-icon"></span> <span class="syn-icon"></span>
<span class="syn-name">@s1</span> <span class="syn-name">@s</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> </div>
<div class="pairing-info"> <div class="pairing-info">
<span class="pairing-label">Dual-Syndicate</span> <span class="pairing-label">Syndicate</span>
</div> </div>
</div> </div>
} }
</div> </div>
</div> </div>
<SyndicatePairDialog Doc="selectedDoc" OnClose="ClosePairing" /> <SyndicateDialog Doc="selectedDoc" OnClose="CloseSyndicate" OnNavigate="HandleNavigate" />
<CardDialog Card="selectedCard" OnClose="CloseCard" OnNavigate="HandleNavigate" />
<GenericDocDialog Doc="selectedGenericDoc" OnClose="CloseGenericDoc" />
@code { @code {
private List<DocData> pairings = []; private List<DocData> syndicates = [];
private DocData? selectedDoc; private DocData? selectedDoc;
private CardData? selectedCard;
private DocData? selectedGenericDoc;
protected override void OnInitialized() protected override void OnInitialized()
{ {
pairings = DocDatabase.Docs syndicates = DocDatabase.Docs
.Where(d => d.Category == "Syndicate Pairings") .Where(d => d.Category == "Syndicate")
.OrderBy(d => d.Title) .OrderBy(d => d.Title)
.ToList(); .ToList();
} }
private void OpenPairing(DocData doc) private void OpenSyndicate(DocData doc)
{ {
selectedDoc = doc; selectedDoc = doc;
} }
private void ClosePairing() private void CloseSyndicate()
{ {
selectedDoc = null; selectedDoc = null;
} }
private void CloseCard()
{
selectedCard = null;
}
private void CloseGenericDoc()
{
selectedGenericDoc = null;
}
private void HandleNavigate(string name)
{
var card = CardDatabase.Cards.FirstOrDefault(c => string.Equals(c.Name, name, StringComparison.OrdinalIgnoreCase));
if (card != null)
{
if (card.Category is "Agent" or "Spell")
{
selectedCard = card;
}
else
{
selectedGenericDoc = new DocData { Title = card.Name, Content = card.Description ?? "", Category = card.Category };
}
StateHasChanged();
return;
}
var doc = DocDatabase.Docs.FirstOrDefault(d => string.Equals(d.Title, name, StringComparison.OrdinalIgnoreCase));
if (doc != null)
{
selectedGenericDoc = doc;
StateHasChanged();
}
}
} }
+2 -120
View File
@@ -11,7 +11,7 @@
.pairings-grid { .pairings-grid {
display: grid; display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
gap: 1.5rem; gap: 1.5rem;
} }
@@ -56,6 +56,7 @@
border-radius: 50%; border-radius: 50%;
background: currentColor; background: currentColor;
box-shadow: 0 0 10px currentColor; box-shadow: 0 0 10px currentColor;
display: inline-block;
} }
.syn-name { .syn-name {
@@ -64,12 +65,6 @@
color: var(--text-primary); color: var(--text-primary);
} }
.pairing-separator {
color: var(--text-muted);
font-size: 0.8rem;
opacity: 0.5;
}
.pairing-info { .pairing-info {
border-top: 1px solid var(--border); border-top: 1px solid var(--border);
padding-top: 0.75rem; padding-top: 0.75rem;
@@ -85,15 +80,6 @@
color: var(--text-muted); 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 */ /* Syndicate Colors */
.singularity { color: #ffffff; } .singularity { color: #ffffff; }
.phasetide { color: #2196f3; } .phasetide { color: #2196f3; }
@@ -111,107 +97,3 @@
padding: 1rem; 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; }
}
+32 -45
View File
@@ -13,26 +13,12 @@
"state": { "state": {
"type": "markdown", "type": "markdown",
"state": { "state": {
"file": "Syndicate Pairings/Lifeblood - Splintergleam.md", "file": "Immortalized/[[Splintergleam]]/Boatswain Corvus.md",
"mode": "source", "mode": "source",
"source": false "source": false
}, },
"icon": "lucide-file", "icon": "lucide-file",
"title": "Lifeblood - Splintergleam" "title": "Boatswain Corvus"
}
},
{
"id": "c7d8467472493004",
"type": "leaf",
"state": {
"type": "markdown",
"state": {
"file": "Deplete.md",
"mode": "source",
"source": false
},
"icon": "lucide-file",
"title": "Deplete"
} }
} }
] ]
@@ -67,7 +53,7 @@
"state": { "state": {
"type": "search", "type": "search",
"state": { "state": {
"query": "Unstoppable Grow", "query": "Boatswain",
"matchingCase": false, "matchingCase": false,
"explainSearch": false, "explainSearch": false,
"collapseAll": false, "collapseAll": false,
@@ -88,7 +74,8 @@
"title": "Bookmarks" "title": "Bookmarks"
} }
} }
] ],
"currentTab": 1
} }
], ],
"direction": "horizontal", "direction": "horizontal",
@@ -199,34 +186,37 @@
}, },
"active": "852a1aaf6b54ec43", "active": "852a1aaf6b54ec43",
"lastOpenFiles": [ "lastOpenFiles": [
"Syndicate Pairings/Lifeblood - Sungrace.md", "Syndicate/Splintergleam.md",
"Syndicate Pairings/Lifeblood - Splintergleam.md", "Syndicate/Sungrace.md",
"Syndicate Pairings/Lifeblood - Singularity.md", "Syndicate/Singularity.md",
"Syndicate Pairings/Phasetide - Silence.md", "Syndicate/Silence.md",
"Syndicate Pairings/Phasetide - Singularity.md", "Syndicate/Lifeblood.md",
"Syndicate Pairings/Phasetide - Splintergleam.md", "Syndicate/Phasetide.md",
"Keyword/Surge.md",
"Keyword/Siphon.md",
"Keyword/Refresh.md",
"Keyword/Overpower.md",
"Keyword/Heal.md",
"Keyword/Blitz.md",
"Keyword/Breakdown.md",
"Timeline.md",
"images/Violet Inquisitioner.png",
"Syndicate Pairings/Splintergleam - Sungrace.md",
"Syndicate Pairings/Singularity - Sungrace.md",
"Syndicate Pairings/Singularity - Splintergleam.md",
"Syndicate Pairings/Silence - Sungrace.md",
"Syndicate Pairings/Silence - Splintergleam.md",
"Syndicate Pairings/Phasetide - Sungrace.md", "Syndicate Pairings/Phasetide - Sungrace.md",
"Syndicate Pairings/Silence - Singularity.md", "Syndicate Pairings/Silence - Singularity.md",
"Syndicate Pairings/Silence - Splintergleam.md", "Syndicate Pairings/Phasetide - Splintergleam.md",
"Syndicate Pairings/Silence - Sungrace.md", "Syndicate Pairings/Phasetide - Singularity.md",
"Syndicate Pairings/Singularity - Splintergleam.md", "Syndicate Pairings/Phasetide - Silence.md",
"Syndicate Pairings/Singularity - Sungrace.md", "Syndicate Pairings/Lifeblood - Sungrace.md",
"Syndicate Pairings/Splintergleam - Sungrace.md", "Syndicate Pairings/Lifeblood - Splintergleam.md",
"Syndicate Pairings/Lifeblood - Silence.md", "Syndicate Pairings",
"Syndicate Pairings/Lifeblood - Phasetide.md", "images/Violent Inquisitioner.png",
"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 Telekinetic Burn.md",
"Deck/Precon Sprouts.md",
"_Debuff.base", "_Debuff.base",
"Deck/Big Energy.md",
"Deck/Aggro.md",
"Deck/Kyln Control.md",
"Pass", "Pass",
"_Store.base", "_Store.base",
"_Timeline.base", "_Timeline.base",
@@ -234,7 +224,6 @@
"Quest", "Quest",
"Store", "Store",
"Currency", "Currency",
"_Factions.base",
"Decks/Rewind Me.canvas", "Decks/Rewind Me.canvas",
"images/Wolf.png", "images/Wolf.png",
"images/Seedling.png", "images/Seedling.png",
@@ -244,8 +233,6 @@
"images/Supernova.png", "images/Supernova.png",
"images/Sunshock.png", "images/Sunshock.png",
"images/Soothing Glow.png", "images/Soothing Glow.png",
"images/Radiant Channeling.png",
"images/Paradox Capacitor.png",
"_Agents.canvas" "_Agents.canvas"
] ]
} }
-8
View File
@@ -1,8 +0,0 @@
---
category: Faction
---
Concepts:
- Wide Boards
- Overpower
-8
View File
@@ -1,8 +0,0 @@
---
category: Faction
---
Concepts:
- Healing
-
-8
View File
@@ -1,8 +0,0 @@
---
category: Faction
---
Concepts:
- Activatable Agents
-
-7
View File
@@ -1,7 +0,0 @@
---
category: Faction
---
Concepts:
- Discard
-11
View File
@@ -1,11 +0,0 @@
---
category: Faction
---
# Self Damage
Can hurt it's own units for power. Has [[Bleed]] to make reoccurring damage on units very easy.
# Core Damage
Can hurt both cores easily. Has [[Keyword/Breakdown]] to take advantage of having an injured core for more damage.
-14
View File
@@ -1,14 +0,0 @@
---
category: Faction
---
# Energy Reserves
Care about always having banked energy. Spells can refund themselves when using [[Energy Reserves]], or get stronger is
you [[Overflow]] your energy reserve.
Concepts:
- Ramp
- Using and Replenishing Reserved Energy
-
+49
View File
@@ -0,0 +1,49 @@
---
category: Syndicate
---
# Timelines
## Deadly Fauna
Gives everything +1 / +1.
## Abundant Growth
Gives everything [[Overpower]].
# Mechanics
## Flourish
Flourish.
## Combat
Combat.
## Evasive
Evasive.
## Sprout
Sprout.
# Wolves
Wolves.
# Key Cards
## Defensive Cards:
- [[Bloom]]
- [[Mulch]]
- [[Focused Adaptation]]
- [[Symbiosis]]
## Threats
- [[Glade Grazers]]
- [[Sapling Dryad]]
- [[Spark of Bounty]]
- [[Glasswinged Monarch]]
- [[Zealot of the Hunt]]
- [[Hungry Tyrannosaur]]
+69
View File
@@ -0,0 +1,69 @@
---
category: Syndicate
---
# Timelines
## The One True Timeline
Heals everything 1 health each turn.
A lot of Phasetide agents simply immortalize in this timeline. And a lot of Phasetide agents start damage to take advantage of this healing effect.
And some Phasetide agents will hate it when your not in the timeline. Such as [[Shattersmith]], which will flat out die if too many timelines exist in the stack that are not The One True timeline.
# Mechanics
## Healing
Healing.
Cards:
- [[Bearer of the Broth]]
Payoff:
- [[Bareknuckle Inquisitor]]
## Rewind
Rewind.
Cards:
- [[Temple Analyst]]
Payoff:
- [[Divergence Assassin]]
## Phase
Phase.
Cards:
- [[Set in Stone]]
Payoff:
- [[Karmic Debtor]]
## Reduce Cost
Reduce cost.
Cards:
- [[Slow Convert]]
Payoff:
- [[Stern Arbiter]]
# Key Cards
## Defensive Cards:
- [[Prayer of Rescue]]
- [[Invigorating Balm]]
- [[Set in Stone]]
- [[Out of Line]]
## Threats
- [[Temple Analyst]]
- [[Consummate Conspirator]]
- [[Bareknuckle Inquisitor]]
- [[Divergence Assassin]]
- [[Stern Arbiter]]
+80
View File
@@ -0,0 +1,80 @@
---
category: Syndicate
---
# Timelines
## Voiceless Skies
Causes it so that if you have one agent in play, that agent gets +3 / +3.
With deplete, you can ensure the player ideally can never properly take advantage of the timeline.
It turn on some of your key board survival cards, like [[Braindead Bouncer]] and [[Nameless Spirit]].
It also notably interacts with [[Phase]]. So you can phase your [[Sensory Deprivation Pod]] to potentially activate Voiceless Skies for your other blocker, or use [[Bury the Evidence]] if you have two 1 / 1 cards and need to remove one.
# Mechanics
## Deplete
Prevents a card from attacking and blocking if they have not already attacked or blocked. Also prevents a card from being activated, although outside of [[Mind Over Matter]] during [[Voiceless Sky]], the opposing agent can always activate in a response.
Cards:
- [[Return to Stillness]]
Payoff:
- [[Telepathic Scavenger]]
## Mute
Can remove all keywords and effects on an agent. Note the order matters. If you mute a target, and an effect gets added on after, the agent will have the newly added effect while still being considered muted.
Cards:
- [[Muffle]]
Payoff:
- [[Destiny Ripper]]
## Revival
Can bring cards back from the dead. Currently mostly a mechanic in practice for keeping [[Somnus, the Dreaming]] alive.
Cards:
- [[The Forgotten Tale]]
Payoff:
- [[Solemn Attendant]]
## Spell Duplication
Has access to duplicating spells.
Cards:
- [[Disciplined Student]]
Payoff:
- [[Overburdened Scribe]]
## Activations
Cards with strong activation effects. And it's revival package also allows you to kill off and revive your own cards to activate them again in a fresh state.
Cards:
- [[Death Jockey]]
Payoff:
- [[Rotting Rocker]]
# Key Cards
## Defensive Cards:
- [[Hush Now]]
- [[Spread the Sickness]]
- [[Pressure Spike]]
- [[Muffle]]
## Threats
- [[Rotting Rocker]]
- [[Telepathic Scavenger]]
- [[Sensory Deprivation Pod]]
- [[Overburdened Scribe]]
- [[The Forgotten Tale]]
+66
View File
@@ -0,0 +1,66 @@
---
category: Syndicate
---
# Timelines
## Erudite Beacon
Causes you to draw a card when you shift to it, and a card every round. You will have many cards in hand, so if you play in this timeline, you need ways to spend your hand to avoid erasing cards off the top of your deck.
[[Enlightened Refugee]] is the simplest way to shift to this [[Timeline]].
# Mechanics
## Discard
Has a lot of cards that can be removed from hand for added benefits, and a lot of cards that can discard for additional card draw.
Cards:
- [[Efficient Scrapbot]]
Payoff:
- [[Rapid Iteration]]
## Disarm
Can reduce agents attack values to zero. Way to reduce damage for a turn or win combat trades.
Cards:
- [[Frontline Fellowship]]
Payoff:
- [[Traffic Conductor]]
## Agent Duplication
Has access to duplicating agents.
Cards:
- [[Chronal Scan]]
Payoff:
- [[Springloaded Hopper]]
## Pocket Scout
1 / 1 token that can swarm the board or chump block.
Cards:
- [[Enthusiastic Bot-Poke]]
Payoff:
- [[Scattered Helpers]]
# Key Cards
## Defensive Cards:
- [[Timestop]]
- [[Rapid Iteration]]
- [[Frontline Fellowship]]
## Threats
- [[Roiling Amalgam]]
- [[Rogue Amalgam]]
- [[Quicksilver Khaela]]
- [[Scuttling Spares]]
- [[Springloaded Hopper]]
+48
View File
@@ -0,0 +1,48 @@
---
category: Syndicate
---
# Timelines
## Volcanic Rivers
Deals 1 damage to each core when shifted to.
## Torment
# Mechanics
## Bleed
Bleed.
## Big Timeline Stack
Big Timeline Stack
## Fervor
Fervor
## Self Wound
Self Wound
# Key Cards
## Defensive Cards:
- [[Bathe in Flames]]
- [[Circle of Strife]]
- [[Paradox Cacophony]]
- [[Fervor]]
- [[Sanguine Resurgence]]
## Threats
- [[Pit Dog]]
- [[Bright-Eyed Supplicant]]
- [[Blazing Shifter]]
- [[Toothy Pugilist]]
- [[Devoted Bloodletter]]
- [[Hungry Engine]]
- [[Frontline Juggernaut]]
- [[Brutal Reveler]]
- [[Gilded Behemoth]]
+52
View File
@@ -0,0 +1,52 @@
---
category: Syndicate
---
# Timelines
## Star Siphon
Refill your energy reserves every turn. Not, this will not overflow your energy reserve if your energy reserve is full and you otherwise have no other energy. Now if you have 1 energy, and 0 reserves, even though you have 2 less mana, it will overflow. So your encouraged to spend all your reserves and always leave one normal mana left over a turn to trigger your overflows.
# Mechanics
## Overflow
Energy Reserves.
## Energy Reserves
Energy Reserves.
## Spend 12+ Energy
Spend 12+ Energy.
## Ramp
Ramp.
# Spells
Spells.
## Blitz
Blitz.
# Key Cards
## Defensive Cards:
- [[Soothing Glow]]
- [[Chaos Control]]
- [[Radiant Channeling]]
- [[Supernova]]
- [[Throw into the Sun]]
- [[Entropy's End]]
## Threats
- [[Canine Adjutant]]
- [[Limit Breaker]]
- [[Debris Collector]]
- [[Jury of the Second Law]]
- [[Lightsteel Colossus]]
- [[APEX Starcruise]]
- [[Devourer Spawn]]
+4
View File
@@ -0,0 +1,4 @@
---
category: Rule
description: Provides a passive effect to all players. Only one timeline can be active at a time. The play who shifts to the timeline is considered the owner. The owner can be a bit of a confusing mechanic, but technically it can matter for some cards cares about WHO is triggering the effect. Like a timeline that deals damage and a card the counts the damage you have dealt.
---