diff --git a/Chrono/Web/Components/CardDialog.razor.css b/Chrono/Web/Components/CardDialog.razor.css
index 4f24121..fa45919 100644
--- a/Chrono/Web/Components/CardDialog.razor.css
+++ b/Chrono/Web/Components/CardDialog.razor.css
@@ -235,7 +235,7 @@
text-decoration-color: #7b73ff;
}
-@
+
@media (max-width: 768px) {
.detail-layout {
flex-direction: column;
diff --git a/Chrono/Web/Pages/Cards.razor b/Chrono/Web/Pages/Cards.razor
index 6952644..defaacc 100644
--- a/Chrono/Web/Pages/Cards.razor
+++ b/Chrono/Web/Pages/Cards.razor
@@ -26,11 +26,15 @@
@onclick='@(() => SetCategory("Immortalized"))'>
Immortalized
-
+
+
+
+ Linked
+
+
+
+
+
@@ -206,21 +210,70 @@
private IEnumerable ApplyFilters()
{
- var filtered = allCards.Where(c =>
+ var primarySet = allCards.Where(c =>
c.MatchesSearch(search) &&
(categoryFilter == "" || c.Category == categoryFilter) &&
(factionFilter == "" || c.Faction == factionFilter) &&
(costFilter == "" || c.Cost?.ToString() == costFilter)
);
- return sortBy switch
+ var sortedPrimary = sortBy switch
{
- "Cost" => sortDescending ? filtered.OrderByDescending(c => c.Cost) : filtered.OrderBy(c => c.Cost),
- "Efficiency" => sortDescending ? filtered.OrderByDescending(c => c.StatEfficiency) : filtered.OrderBy(c => c.StatEfficiency),
- "Attack" => sortDescending ? filtered.OrderByDescending(c => c.Attack) : filtered.OrderBy(c => c.Attack),
- "Health" => sortDescending ? filtered.OrderByDescending(c => c.Health) : filtered.OrderBy(c => c.Health),
- _ => sortDescending ? filtered.OrderByDescending(c => c.Name) : filtered.OrderBy(c => c.Name)
+ "Cost" => sortDescending ? primarySet.OrderByDescending(c => c.Cost) : primarySet.OrderBy(c => c.Cost),
+ "Efficiency" => sortDescending ? primarySet.OrderByDescending(c => c.StatEfficiency) : primarySet.OrderBy(c => c.StatEfficiency),
+ "Attack" => sortDescending ? primarySet.OrderByDescending(c => c.Attack) : primarySet.OrderBy(c => c.Attack),
+ "Health" => sortDescending ? primarySet.OrderByDescending(c => c.Health) : primarySet.OrderBy(c => c.Health),
+ _ => sortDescending ? primarySet.OrderByDescending(c => c.Name) : primarySet.OrderBy(c => c.Name)
};
+
+ if (!agentLink)
+ {
+ return sortedPrimary;
+ }
+
+ var result = new List();
+ var seen = new HashSet();
+ foreach (var card in sortedPrimary)
+ {
+ if (seen.Contains(card.Name)) continue;
+
+ // Determine the "Chain Head" (the Agent)
+ var head = card;
+ if (card.Category == "Immortalized" && card.ImmortalizeFrom != null)
+ {
+ var source = allCards.FirstOrDefault(c => c.Name == card.ImmortalizeFrom);
+ if (source != null) head = source;
+ }
+
+ // Add the chain starting from head
+ if (!seen.Contains(head.Name))
+ {
+ result.Add(head);
+ seen.Add(head.Name);
+ }
+
+ if (head.ImmortalizeTo != null)
+ {
+ foreach (var targetName in head.ImmortalizeTo)
+ {
+ if (seen.Contains(targetName)) continue;
+ var linked = allCards.FirstOrDefault(c => c.Name == targetName);
+ if (linked != null)
+ {
+ result.Add(linked);
+ seen.Add(linked.Name);
+ }
+ }
+ }
+
+ // Ensure the original card is added if it was somehow not part of the head's chain
+ if (!seen.Contains(card.Name))
+ {
+ result.Add(card);
+ seen.Add(card.Name);
+ }
+ }
+ return result;
}
private void SetImmortal(string imm)
@@ -253,6 +306,12 @@
private void ToggleAgentLink()
{
agentLink = !agentLink;
+ OnToggleAgentLink();
+ }
+
+ private void OnToggleAgentLink()
+ {
+ _ = AnalyticsService.TrackEvent("toggle_agent_link", new { enabled = agentLink });
}
private void ClearCostFilter()
diff --git a/Chrono/Web/Pages/Cards.razor.css b/Chrono/Web/Pages/Cards.razor.css
index f4b422c..a938d0c 100644
--- a/Chrono/Web/Pages/Cards.razor.css
+++ b/Chrono/Web/Pages/Cards.razor.css
@@ -74,6 +74,62 @@
font-size: 0.9rem;
}
+.category-divider {
+ width: 1px;
+ height: 1.5rem;
+ background: var(--border);
+ margin: 0 0.6rem;
+ align-self: center;
+ opacity: 0.6;
+}
+
+.tab.link-toggle {
+ border-style: dashed;
+ gap: 0.6rem;
+ user-select: none;
+ padding-right: 0.8rem;
+ margin-left: 0.2rem;
+}
+
+.tab.link-toggle.active {
+ border-style: solid;
+ background: rgba(108, 99, 255, 0.1);
+ border-color: var(--accent);
+ color: var(--text-primary);
+}
+
+.toggle-switch {
+ width: 28px;
+ height: 16px;
+ background: var(--bg-hover);
+ border-radius: 10px;
+ position: relative;
+ transition: all 0.2s;
+ border: 1px solid var(--border);
+ flex-shrink: 0;
+}
+
+.toggle-switch.on {
+ background: var(--accent);
+ border-color: rgba(255, 255, 255, 0.2);
+}
+
+.toggle-switch::after {
+ content: '';
+ position: absolute;
+ width: 10px;
+ height: 10px;
+ background: white;
+ border-radius: 50%;
+ top: 2px;
+ left: 2px;
+ transition: transform 0.2s;
+}
+
+.toggle-switch.on::after {
+ transform: translateX(12px);
+}
+
/* ── Filter Bar ── */
.filter-bar {
display: flex;
diff --git a/chrono.docs/.obsidian/workspace.json b/chrono.docs/.obsidian/workspace.json
index 3397b4f..68f81ff 100644
--- a/chrono.docs/.obsidian/workspace.json
+++ b/chrono.docs/.obsidian/workspace.json
@@ -13,12 +13,12 @@
"state": {
"type": "markdown",
"state": {
- "file": "Deck/Overflowing Security.md",
+ "file": "Agent/[[Phasetide]]/Stern Arbiter.md",
"mode": "source",
"source": false
},
"icon": "lucide-file",
- "title": "Overflowing Security"
+ "title": "Stern Arbiter"
}
}
]
@@ -53,7 +53,7 @@
"state": {
"type": "search",
"state": {
- "query": "The Ripper",
+ "query": "...",
"matchingCase": false,
"explainSearch": false,
"collapseAll": false,
@@ -74,12 +74,12 @@
"title": "Bookmarks"
}
}
- ]
+ ],
+ "currentTab": 1
}
],
"direction": "horizontal",
- "width": 331.5,
- "collapsed": true
+ "width": 331.5
},
"right": {
"id": "47f3bcd7aed212ab",
@@ -186,8 +186,11 @@
},
"active": "852a1aaf6b54ec43",
"lastOpenFiles": [
- "Deck/Big Energy.md",
+ "Immortalized/[[Sungrace]]/Time Devourer Soval.md",
+ "Spell/[[Phasetide]]/By the Numbers.md",
+ "Immortalized/[[Splintergleam]]/Boatswain Corvus.md",
"Deck/Overflowing Security.md",
+ "Deck/Big Energy.md",
"Agent/[[Lifeblood]]/Aardvark Precinct Captain.md",
"Agent/[[Lifeblood]]/Egg Tender.md",
"Quest/Daily Wins.md",
@@ -212,9 +215,6 @@
"Rule/Fast.md",
"_Store.base",
"_Timeline.base",
- "Store/Core Set Packs x1.md",
- "Store/Core Set Packs x5.md",
- "Store/Core Set Packs x10.md",
"_Quests.base",
"Quest",
"Store",
diff --git a/chrono.docs/Agent/[[Phasetide]]/Stern Arbiter.md b/chrono.docs/Agent/[[Phasetide]]/Stern Arbiter.md
index 829b562..6bf62bf 100644
--- a/chrono.docs/Agent/[[Phasetide]]/Stern Arbiter.md
+++ b/chrono.docs/Agent/[[Phasetide]]/Stern Arbiter.md
@@ -6,8 +6,8 @@ rarity: Lost
cost: 7
attack: 5
health: 5
-description: "[[Play]]: Return an Action from your Graveyard to your hand. It costs 3 less and has \"If I would be put into a Graveyard, instead [[Erase]] me.\"."
-immortalizeWhen: "I've seen you play 3+ cards wi..."
+description: '[[Play]]: Return an Action from your Graveyard to your hand. It costs 3 less and has "If I would be put into a Graveyard, instead [[Erase]] me.".'
+immortalizeWhen: I've seen you play 3+ cards with reduced cost.
immortalizeTo:
- "[[Grand Judge Dhael]]"
immortalizeFrom: N/A
diff --git a/chrono.docs/Immortalized/[[Splintergleam]]/Boatswain Corvus.md b/chrono.docs/Immortalized/[[Splintergleam]]/Boatswain Corvus.md
index e09fb2a..13ffb43 100644
--- a/chrono.docs/Immortalized/[[Splintergleam]]/Boatswain Corvus.md
+++ b/chrono.docs/Immortalized/[[Splintergleam]]/Boatswain Corvus.md
@@ -6,7 +6,7 @@ rarity: Lost
cost: 7
attack: 6
health: 10
-description: "[[Overpower]]. Round Start: Deal 1 to the enemy Core twice. When you deal non-combat damage to the enemy Core, grant the weakest enemy Exposed. [[Core Strike]]: Deal 3 to all enemies and t..."
+description: "[[Overpower]]. Round Start: Deal 1 to the enemy Core twice. When you deal non-combat damage to the enemy Core, grant the weakest enemy Exposed. [[Core Strike]]: Deal 3 to all enemies and the enemy Core.]]"
immortalizeWhen: N/A
immortalizeTo:
- N/A
diff --git a/chrono.docs/Immortalized/[[Sungrace]]/Time Devourer Soval.md b/chrono.docs/Immortalized/[[Sungrace]]/Time Devourer Soval.md
index d01d736..31428d9 100644
--- a/chrono.docs/Immortalized/[[Sungrace]]/Time Devourer Soval.md
+++ b/chrono.docs/Immortalized/[[Sungrace]]/Time Devourer Soval.md
@@ -6,7 +6,7 @@ rarity: Lost
cost: 10
attack: 12
health: 12
-description: "For each Timeline in the Timeline Stack, I gain a random positive keyword. The first time each round I would be destroyed, instead heal me to my maximum durability and destroy the..."
+description: For each Timeline in the Timeline Stack, I gain a random positive keyword. The first time each round I would be destroyed, instead heal me to my maximum durability and destroy the current Timeline.
immortalizeWhen: N/A
immortalizeTo:
- N/A
diff --git a/chrono.docs/Spell/[[Phasetide]]/By the Numbers.md b/chrono.docs/Spell/[[Phasetide]]/By the Numbers.md
index 7f5e837..b5053e5 100644
--- a/chrono.docs/Spell/[[Phasetide]]/By the Numbers.md
+++ b/chrono.docs/Spell/[[Phasetide]]/By the Numbers.md
@@ -4,7 +4,7 @@ category: Spell
artist: Bi Pi
rarity: Common
cost: 2
-description: "Reveal the top card of both decks. Create a copy of the card with the higher cost in hand, then shuffle both cards into their respective decks. In a tie, create a copy of both card..."
+description: Reveal the top card of both decks. Create a copy of the card with the higher cost in hand, then shuffle both cards into their respective decks. In a tie, create a copy of both cards.
faction: "[[Phasetide]]"
set: "[[Core Set]]"
speed: "[[Immediate]]"