This commit is contained in:
6d486f49
2026-08-17 00:45:08 -04:00
parent 3a2f112a73
commit 1d6207fbfe
154 changed files with 126007 additions and 47385 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
@namespace Shared.Components
@inject Shared.Services.CardPreviewService PreviewService
@inject CardPreviewService PreviewService
<div class="deck-card-row">
@foreach (var group in Cards.GroupBy(x => x))
+7 -5
View File
@@ -1,12 +1,11 @@
@using Shared.Services
@using Model
@inject CardPreviewService PreviewService
@inject CardPreviewService PreviewService
@if (PreviewService.IsVisible && PreviewService.HoveredCard != null)
{
<div class="card-preview-container" style="left: @(PreviewService.X + 20)px; top: @(PreviewService.Y - 100)px;">
<div class="card-preview-card">
<img src="@PreviewService.HoveredCard.ImagePath" alt="@PreviewService.HoveredCard.Name" class="preview-image" />
<img src="@PreviewService.HoveredCard.ImagePath" alt="@PreviewService.HoveredCard.Name"
class="preview-image"/>
<div class="preview-overlay">
<div class="preview-name">@PreviewService.HoveredCard.Name</div>
<div class="preview-meta">
@@ -25,7 +24,8 @@
<div class="preview-stats">
@if (PreviewService.HoveredCard.Attack.HasValue)
{
<span class="stat-item"><i class="bi bi-shield-shaded"></i> @PreviewService.HoveredCard.Attack</span>
<span class="stat-item"><i
class="bi bi-shield-shaded"></i> @PreviewService.HoveredCard.Attack</span>
}
@if (PreviewService.HoveredCard.Health.HasValue)
{
@@ -39,6 +39,7 @@
}
@code {
protected override void OnInitialized()
{
PreviewService.OnChange += StateHasChanged;
@@ -48,4 +49,5 @@
{
PreviewService.OnChange -= StateHasChanged;
}
}
+11 -5
View File
@@ -11,7 +11,7 @@
border: 2px solid #333;
border-radius: 12px;
overflow: hidden;
box-shadow: 0 10px 30px rgba(0,0,0,0.5);
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.5);
animation: previewFadeIn 0.2s ease-out;
}
@@ -22,7 +22,7 @@
.preview-overlay {
padding: 10px;
background: linear-gradient(to top, rgba(0,0,0,0.9), transparent);
background: linear-gradient(to top, rgba(0, 0, 0, 0.9), transparent);
position: absolute;
bottom: 0;
left: 0;
@@ -33,7 +33,7 @@
font-weight: bold;
color: white;
font-size: 1.1rem;
text-shadow: 0 2px 4px rgba(0,0,0,0.8);
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.8);
}
.preview-meta {
@@ -88,6 +88,12 @@
}
@keyframes previewFadeIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@@ -1,6 +1,5 @@
@using System.Text.RegularExpressions
@using Microsoft.AspNetCore.Components.Rendering
@using Model
@namespace Shared.Components
@if (Doc != null)
+7
View File
@@ -7,6 +7,13 @@ public static class CardDatabase
{
public static readonly System.Collections.Generic.List<CardData> Cards =
[
new()
{
Name = "Playing Mono",
Category = "Theory",
Archetypes = [
],
},
new()
{
Name = "Core Charges",
+13 -3
View File
@@ -7,6 +7,16 @@ public static class DocDatabase
{
public static readonly System.Collections.Generic.List<DocData> Docs =
[
new()
{
Title = "Playing Mono",
Category = "Theory",
Content = "Currently, Chrono CCG has no real mono support. No card that improves it's quality if you only play cards from one\r\nsyndicate, no card that forces mono play, nothing that even mentions the concept.\r\n\r\nNow, the biggest obvious reason to not play mono is your doubling your card pool selection, increasing the amount of\r\nsynergies you can make, give yourself access to more powerful tools and threats, and also just adding cognitive burden\r\non your opponent in ladder as they have more cards to play around given the standard blind deck format.\r\n\r\nSo based on these assumptions and past card games, you would view no reason to go mono, but I believe there is one. The\r\ndiver system. Two cards you are always guaranteed to have in your opening hand, and in theory, the greatest amount of\r\nsynergy between cards should be cards within the same syndicate.\r\n\r\nExamples:\r\n\r\nIn Phasetide, [[Fervent Follower]] and [[Shattersmith]] are two cards you always want in your opening hand as the chance\r\nfor them to have value lessens over time.\r\n\r\nIn Singularity, [[Efficient Scrapbot]] and [[Enhanced Retriever]] with get you a strong start in draw and add some\r\nblockers or potential let you get in some early damage.\r\n\r\nIn Splittergleam, [[Persistent Squire]] and [[Toothy Pugilist]] so you always have some bleed creators.",
Frontmatter = new()
{
{ "category", "Theory" },
},
},
new()
{
Title = "Core Charges",
@@ -164,7 +174,7 @@ public static class DocDatabase
{
Title = "No New Timelines",
Category = "Design",
Content = "It was mentioned in one of the developer interviews that additional Timelines per syndicate are not intended. \n\nWhich is great, but very unexpected, given the nature of most existing card games to always invent new mechanics a set. So the idea that something like [[Splintergleam]] will always have [[Volcanic Rivers]] and [[Torment]] are not expected. One would expect timelines outside of that lava theme, and that blood mechanic. Or again, how this means [[Silence]] will only ever have access to one timeline, [[Voiceless Sky]].\n\nThe reason I like this is simple, other card games throwing out mechanics always feels irksome. Like mechanics often feel like they don't get enough support before they are thrown out, so your left in an unfinished state. And if a particular mechanic is liked, well unless it's \"evergreen,\" it's probably gone. So I like these timelines being evergreen.\n\nPoint is, get used to these timelines.\n\nSource: [Quick Chat | !Chrono | Game Dev QnA Show at 3:15PT :) - Twitch](https://www.twitch.tv/videos/2844466601)\n\nThey did, also to my surprise, mention more Syndicates are a possibility, so if new timelines do come into existence, it will probably be via that vector.\n\n5-6 factions seems typical for card games, so I wouldn't expect a 7th given the obvious problems that can cause. Such as the additional factions feeling they lack cards. But the devs in interview clearly do not feel this is the case. Because mono isn't really a thing, syndicates are mostly used in pairs, so any new syndicate pairing would have access to all the cards in it's pair.\n\nAnyways, new syndicates with new timelines belonging to them. Interesting to think about.",
Content = "It was mentioned in one of the developer interviews that additional Timelines per syndicate are not intended.\r\n\r\nWhich is great, but very unexpected, given the nature of most existing card games to always invent new mechanics a set.\r\nSo the idea that something like [[Splintergleam]] will always have [[Volcanic Rivers]] and [[Torment]] are not expected.\r\nOne would expect timelines outside of that lava theme, and that blood mechanic. Or again, how this means [[Silence]]\r\nwill only ever have access to one timeline, [[Voiceless Sky]].\r\n\r\nThe reason I like this is simple, other card games throwing out mechanics always feels irksome. Like mechanics often\r\nfeel like they don't get enough support before they are thrown out, so your left in an unfinished state. And if a\r\nparticular mechanic is liked, well unless it's \"evergreen,\" it's probably gone. So I like these timelines being\r\nevergreen.\r\n\r\nPoint is, get used to these timelines.\r\n\r\nSource: [Quick Chat | !Chrono | Game Dev QnA Show at 3:15PT :) - Twitch](https://www.twitch.tv/videos/2844466601)\r\n\r\nThey did, also to my surprise, mention more Syndicates are a possibility, so if new timelines do come into existence, it\r\nwill probably be via that vector.\r\n\r\n5-6 factions seems typical for card games, so I wouldn't expect a 7th given the obvious problems that can cause. Such as\r\nthe additional factions feeling they lack cards. But the devs in interview clearly do not feel this is the case. Because\r\nmono isn't really a thing, syndicates are mostly used in pairs, so any new syndicate pairing would have access to all\r\nthe cards in it's pair.\r\n\r\nAnyways, new syndicates with new timelines belonging to them. Interesting to think about.",
Frontmatter = new()
{
{ "category", "Design" },
@@ -461,7 +471,7 @@ public static class DocDatabase
{
Title = "Daily Wins",
Category = "Quest",
Content = "1: 30cc 30xp\r\n2: 20cc 25xp\r\n3: 20cc 20xp\r\n4: 10cc 20xp\r\n5: 10cc 15xp\r\n6-12 are all simply 5xp.\r\n\r\nSo the daily total win reward for 12 wins is 90cc/145xp.",
Content = "1: 30cc 30xp 2: 20cc 25xp 3: 20cc 20xp 4: 10cc 20xp 5: 10cc 15xp 6-12 are all simply 5xp.\r\n\r\nSo the daily total win reward for 12 wins is 90cc/145xp.",
Frontmatter = new()
{
{ "category", "Quest" },
@@ -831,7 +841,7 @@ public static class DocDatabase
{
Title = "Silence",
Category = "Syndicate",
Content = "# Timelines\r\n\r\n## Voiceless Skies\r\n\r\nCauses it so that if you have one agent in play, that agent gets +3 / +3.\r\nWith deplete, you can ensure the player ideally can never properly take advantage of the timeline.\r\n\r\nIt turn on some of your key board survival cards, like [[Braindead Bouncer]] and [[Nameless Spirit]].\r\n\r\nIt also notably interacts with [[Phase]]. So you can phase your [[Sensory Deprivation Pod]] to potentially activate\r\nVoiceless Skies for your other blocker, or use [[Bury the Evidence]] if you have two 1 / 1 cards and need to remove one.\r\n\r\n# Mechanics\r\n\r\n## Deplete\r\n\r\nPrevents a card from attacking and blocking if they have not already attacked or blocked. Also prevents a card from\r\nbeing activated, although outside of [[Mind Over Matter]] during [[Voiceless Sky]], the opposing agent can always\r\nactivate in a response.\r\n\r\nCards:\r\n\r\n- [[Return to Stillness]]\r\n\r\nPayoff:\r\n\r\n- [[Telepathic Scavenger]]\r\n\r\n## Mute\r\n\r\nCan remove all keywords and effects on an agent. Note the order matters. If you mute a target, and an effect gets added\r\non after, the agent will have the newly added effect while still being considered muted.\r\n\r\nCards:\r\n\r\n- [[Muffle]]\r\n\r\nPayoff:\r\n\r\n- [[Destiny Ripper]]\r\n\r\n## Revival\r\n\r\nCan bring cards back from the dead. Currently mostly a mechanic in practice for keeping [[Somnus, the Dreaming]] alive.\r\n\r\nCards:\r\n\r\n- [[The Forgotten Tale]]\r\n\r\nPayoff:\r\n\r\n- [[Solemn Attendant]]\r\n\r\n## Spell Duplication\r\n\r\nHas access to duplicating spells.\r\n\r\nCards:\r\n\r\n- [[Disciplined Student]]\r\n\r\nPayoff:\r\n\r\n- [[Overburdened Scribe]]\r\n\r\n## Activations\r\n\r\nCards with strong activation effects. And it's revival package also allows you to kill off and revive your own cards to\r\nactivate them again in a fresh state.\r\n\r\nCards:\r\n\r\n- [[Death Jockey]]\r\n\r\nPayoff:\r\n\r\n- [[Rotting Rocker]]\r\n\r\n# Key Cards\r\n\r\n## Defensive Cards:\r\n\r\n- [[Hush Now]]\r\n- [[Spread the Sickness]]\r\n- [[Pressure Spike]]\r\n- [[Muffle]]\r\n\r\n## Threats\r\n\r\n- [[Rotting Rocker]]\r\n- [[Telepathic Scavenger]]\r\n- [[Sensory Deprivation Pod]]\r\n- [[Overburdened Scribe]]\r\n- [[The Forgotten Tale]]",
Content = "# Timelines\r\n\r\n## Voiceless Skies\r\n\r\nCauses it so that if you have one agent in play, that agent gets +3 / +3. With deplete, you can ensure the player\r\nideally can never properly take advantage of the timeline.\r\n\r\nIt turn on some of your key board survival cards, like [[Braindead Bouncer]] and [[Nameless Spirit]].\r\n\r\nIt also notably interacts with [[Phase]]. So you can phase your [[Sensory Deprivation Pod]] to potentially activate\r\nVoiceless Skies for your other blocker, or use [[Bury the Evidence]] if you have two 1 / 1 cards and need to remove one.\r\n\r\n# Mechanics\r\n\r\n## Deplete\r\n\r\nPrevents a card from attacking and blocking if they have not already attacked or blocked. Also prevents a card from\r\nbeing activated, although outside of [[Mind Over Matter]] during [[Voiceless Sky]], the opposing agent can always\r\nactivate in a response.\r\n\r\nCards:\r\n\r\n- [[Return to Stillness]]\r\n\r\nPayoff:\r\n\r\n- [[Telepathic Scavenger]]\r\n\r\n## Mute\r\n\r\nCan remove all keywords and effects on an agent. Note the order matters. If you mute a target, and an effect gets added\r\non after, the agent will have the newly added effect while still being considered muted.\r\n\r\nCards:\r\n\r\n- [[Muffle]]\r\n\r\nPayoff:\r\n\r\n- [[Destiny Ripper]]\r\n\r\n## Revival\r\n\r\nCan bring cards back from the dead. Currently mostly a mechanic in practice for keeping [[Somnus, the Dreaming]] alive.\r\n\r\nCards:\r\n\r\n- [[The Forgotten Tale]]\r\n\r\nPayoff:\r\n\r\n- [[Solemn Attendant]]\r\n\r\n## Spell Duplication\r\n\r\nHas access to duplicating spells.\r\n\r\nCards:\r\n\r\n- [[Disciplined Student]]\r\n\r\nPayoff:\r\n\r\n- [[Overburdened Scribe]]\r\n\r\n## Activations\r\n\r\nCards with strong activation effects. And it's revival package also allows you to kill off and revive your own cards to\r\nactivate them again in a fresh state.\r\n\r\nCards:\r\n\r\n- [[Death Jockey]]\r\n\r\nPayoff:\r\n\r\n- [[Rotting Rocker]]\r\n\r\n# Key Cards\r\n\r\n## Defensive Cards:\r\n\r\n- [[Hush Now]]\r\n- [[Spread the Sickness]]\r\n- [[Pressure Spike]]\r\n- [[Muffle]]\r\n\r\n## Threats\r\n\r\n- [[Rotting Rocker]]\r\n- [[Telepathic Scavenger]]\r\n- [[Sensory Deprivation Pod]]\r\n- [[Overburdened Scribe]]\r\n- [[The Forgotten Tale]]",
Frontmatter = new()
{
{ "category", "Syndicate" },
+1 -1
View File
@@ -1,5 +1,5 @@
@namespace Shared.Pages
@inject Shared.Services.CardPreviewService PreviewService
@inject CardPreviewService PreviewService
@page "/agents"
<PageTitle>Chrono CCG - Agents</PageTitle>
+236 -235
View File
@@ -1,6 +1,6 @@
@namespace Shared.Pages
@inject CardRenderingService CardRenderer
@inject Shared.Services.CardPreviewService PreviewService
@inject CardPreviewService PreviewService
@page "/decks/{Name}"
<PageTitle>Chrono CCG - @(deck?.Name ?? "Deck Details")</PageTitle>
@@ -15,140 +15,141 @@
@if (deck == null)
{
<div class="empty-state">
<i class="bi bi-exclamation-circle"></i>
<p>Deck not found.</p>
</div>
<div class="empty-state">
<i class="bi bi-exclamation-circle"></i>
<p>Deck not found.</p>
</div>
}
else
{
<article class="deck-article">
<header class="deck-header">
<h1>@deck.Name</h1>
<b>Deck Code:</b>
<pre>@deck.DeckCode</pre>
@if (deck.Syndicates.Count > 0)
<article class="deck-article">
<header class="deck-header">
<h1>@deck.Name</h1>
<b>Deck Code:</b>
<pre>@deck.DeckCode</pre>
@if (deck.Syndicates.Count > 0)
{
<div class="deck-factions">
@foreach (var f in deck.Syndicates)
{
<div class="deck-factions">
@foreach (var f in deck.Syndicates)
{
<span class="faction-badge">@f</span>
}
</div>
<span class="faction-badge">@f</span>
}
<div class="deck-card-count">
<span>@deck.Cards.Count cards</span>
<span class="ms-3 badge @(deck.IsValid ? "bg-success" : "bg-danger")">
</div>
}
<div class="deck-card-count">
<span>@deck.Cards.Count cards</span>
<span class="ms-3 badge @(deck.IsValid ? "bg-success" : "bg-danger")">
@(deck.IsValid ? "Valid" : "Invalid")
</span>
@if (!deck.IsValid)
{
<small class="text-danger ms-2 d-block d-md-inline">@deck.ValidationMessage</small>
}
</div>
<ManaCurve Distribution="GetManaDistribution()" MaxCostLabel="10"/>
</header>
@if (deck.Keycards.Count > 0)
{
<section class="deck-section">
<h2><i class="bi bi-star-fill" style="color: var(--gold);"></i> Keycards</h2>
<CardGrid Cards="deck.Keycards" OnSelectCard="SelectCard"/>
</section>
}
@if (deck.Divers.Count > 0)
{
<section class="deck-section">
<h2><i class="bi bi-shuffle"></i> Divers</h2>
<CardGrid Cards="deck.Divers" OnSelectCard="SelectCard"/>
</section>
}
<section class="deck-section">
<h2><i class="bi bi-collection-fill"></i> Cards</h2>
<CardGrid Cards="deck.Cards" OnSelectCard="SelectCard"/>
</section>
@if (deck.Description != null)
{
<section class="deck-section description">
<h2><i class="bi bi-chat-quote-fill"></i> Description</h2>
<div class="deck-description">
@CardRenderer.RenderDescription(deck.Description, SelectCard, doc => PreviewService.SelectDoc(doc), PreviewService)
</div>
</section>
}
<section class="deck-section simulator">
<div class="simulator-header">
<h2><i class="bi bi-play-circle-fill"></i> Opening Hand Simulator</h2>
<div class="simulator-controls">
@if (hand.Count == 0)
{
<button class="btn btn-primary" @onclick="DrawInitialHand">
<i class="bi bi-hand-index-thumb"></i> Draw Hand
</button>
}
else
{
<button class="btn btn-outline-secondary btn-sm" @onclick="DrawInitialHand">
<i class="bi bi-arrow-clockwise"></i> Reset
</button>
@if (!hasMulliganed)
{
<button class="btn btn-warning btn-sm" @onclick="Mulligan" disabled="@(!canMulligan)">
<i class="bi bi-shuffle"></i> Mulligan Selected (@mulliganSelected.Count)
</button>
}
}
</div>
</div>
@if (hand.Count > 0)
@if (!deck.IsValid)
{
<div class="hand-display">
@foreach (var card in hand)
{
var isDiver = deck.Divers.Contains(card.Name);
var isSelected = mulliganSelected.Contains(card);
<div class="hand-card-wrapper @(isDiver ? "diver" : "") @(isSelected ? "selected" : "")"
@onclick="() => ToggleMulliganSelection(card)">
<div class="hand-card-img" @onmouseenter="e => PreviewService.Show(card, e.ClientX, e.ClientY)" @onmouseleave="() => PreviewService.Hide()">
@if (!string.IsNullOrEmpty(card.ImageFile))
{
<img src="cards/@card.ImageFile" alt="@card.Name" />
}
else
{
<div class="card-placeholder">@card.Name</div>
}
</div>
@if (isDiver)
{
<span class="diver-tag">Diver</span>
}
@if (isSelected)
{
<div class="mulligan-overlay"><i class="bi bi-trash"></i></div>
}
</div>
}
</div>
<p class="simulator-hint">
<small class="text-danger ms-2 d-block d-md-inline">@deck.ValidationMessage</small>
}
</div>
<ManaCurve Distribution="GetManaDistribution()" MaxCostLabel="10"/>
</header>
@if (deck.Keycards.Count > 0)
{
<section class="deck-section">
<h2><i class="bi bi-star-fill" style="color: var(--gold);"></i> Keycards</h2>
<CardGrid Cards="deck.Keycards" OnSelectCard="SelectCard"/>
</section>
}
@if (deck.Divers.Count > 0)
{
<section class="deck-section">
<h2><i class="bi bi-shuffle"></i> Divers</h2>
<CardGrid Cards="deck.Divers" OnSelectCard="SelectCard"/>
</section>
}
<section class="deck-section">
<h2><i class="bi bi-collection-fill"></i> Cards</h2>
<CardGrid Cards="deck.Cards" OnSelectCard="SelectCard"/>
</section>
@if (deck.Description != null)
{
<section class="deck-section description">
<h2><i class="bi bi-chat-quote-fill"></i> Description</h2>
<div class="deck-description">
@CardRenderer.RenderDescription(deck.Description, SelectCard, doc => PreviewService.SelectDoc(doc), PreviewService)
</div>
</section>
}
<section class="deck-section simulator">
<div class="simulator-header">
<h2><i class="bi bi-play-circle-fill"></i> Opening Hand Simulator</h2>
<div class="simulator-controls">
@if (hand.Count == 0)
{
<button class="btn btn-primary" @onclick="DrawInitialHand">
<i class="bi bi-hand-index-thumb"></i> Draw Hand
</button>
}
else
{
<button class="btn btn-outline-secondary btn-sm" @onclick="DrawInitialHand">
<i class="bi bi-arrow-clockwise"></i> Reset
</button>
@if (!hasMulliganed)
{
<span>Select non-diver cards to mulligan. You can only mulligan once.</span>
<button class="btn btn-warning btn-sm" @onclick="Mulligan" disabled="@(!canMulligan)">
<i class="bi bi-shuffle"></i> Mulligan Selected (@mulliganSelected.Count)
</button>
}
}
</div>
</div>
@if (hand.Count > 0)
{
<div class="hand-display">
@foreach (var card in hand)
{
var isDiver = deck.Divers.Contains(card.Name);
var isSelected = mulliganSelected.Contains(card);
<div class="hand-card-wrapper @(isDiver ? "diver" : "") @(isSelected ? "selected" : "")"
@onclick="() => ToggleMulliganSelection(card)">
<div class="hand-card-img" @onmouseenter="e => PreviewService.Show(card, e.ClientX, e.ClientY)"
@onmouseleave="() => PreviewService.Hide()">
@if (!string.IsNullOrEmpty(card.ImageFile))
{
<img src="cards/@card.ImageFile" alt="@card.Name"/>
}
else
{
<span class="text-success">Mulligan used.</span>
<div class="card-placeholder">@card.Name</div>
}
</p>
</div>
@if (isDiver)
{
<span class="diver-tag">Diver</span>
}
@if (isSelected)
{
<div class="mulligan-overlay"><i class="bi bi-trash"></i></div>
}
</div>
}
</section>
</article>
</div>
<p class="simulator-hint">
@if (!hasMulliganed)
{
<span>Select non-diver cards to mulligan. You can only mulligan once.</span>
}
else
{
<span class="text-success">Mulligan used.</span>
}
</p>
}
</section>
</article>
}
</div>
@@ -158,139 +159,139 @@
DescriptionRenderer="@(desc => CardRenderer.RenderDescription(desc, SelectCard, doc => PreviewService.SelectDoc(doc), PreviewService))"/>
@code {
[Parameter] public string Name { get; set; } = "";
[Parameter] public string Name { get; set; } = "";
private DeckData? deck;
private CardData? selectedCard;
private DeckData? deck;
private CardData? selectedCard;
private bool HasImage => selectedCard?.ImageFile is { Length: > 0 } && selectedCard.ImageFile != "placeholder.png";
private bool HasImage => selectedCard?.ImageFile is { Length: > 0 } && selectedCard.ImageFile != "placeholder.png";
protected override void OnParametersSet()
protected override void OnParametersSet()
{
var decoded = Uri.UnescapeDataString(Name);
deck = DeckDatabase.Decks.FirstOrDefault(d => d.IsVisible && d.Name == decoded);
}
private static readonly Dictionary<string, CardData> CardLookup = CardDatabase.Cards
.Where(c => !string.IsNullOrWhiteSpace(c.Name))
.GroupBy(c => c.Name, StringComparer.OrdinalIgnoreCase)
.ToDictionary(g => g.Key, g => g.First(), StringComparer.OrdinalIgnoreCase);
private CardData? LookupCard(string cardName)
{
return CardLookup.GetValueOrDefault(cardName);
}
private void SelectCard(CardData? card)
{
selectedCard = card;
}
private void CloseDetail()
{
selectedCard = null;
}
private List<CardData> hand = [];
private readonly HashSet<CardData> mulliganSelected = [];
private bool hasMulliganed;
private bool canMulligan => mulliganSelected.Count > 0 && !hasMulliganed;
private void DrawInitialHand()
{
if (deck == null) return;
hand.Clear();
mulliganSelected.Clear();
hasMulliganed = false;
var random = new Random();
// 1. Get Divers
var divers = deck.Divers.Select(LookupCard).Where(c => c != null).Cast<CardData>().ToList();
// 2. Get Main Deck (excluding divers if they are in the main list, though usually they are separate in data)
var mainDeck = deck.Cards.Select(LookupCard).Where(c => c != null).Cast<CardData>().ToList();
// Add 3 random non-diver cards from main deck
var nonDivers = mainDeck.Where(c => !deck.Divers.Contains(c.Name)).OrderBy(_ => random.Next()).Take(4).ToList();
hand.AddRange(nonDivers);
// Final shuffle of hand for display
hand = hand.OrderBy(_ => random.Next()).ToList();
// Add 2 random divers
if (divers.Count > 0)
{
var decoded = Uri.UnescapeDataString(Name);
deck = DeckDatabase.Decks.FirstOrDefault(d => d.IsVisible && d.Name == decoded);
var newHand = divers.Take(2).ToList();
newHand.AddRange(hand);
hand = newHand;
}
}
private static readonly Dictionary<string, CardData> CardLookup = CardDatabase.Cards
.Where(c => !string.IsNullOrWhiteSpace(c.Name))
.GroupBy(c => c.Name, StringComparer.OrdinalIgnoreCase)
.ToDictionary(g => g.Key, g => g.First(), StringComparer.OrdinalIgnoreCase);
private void ToggleMulliganSelection(CardData card)
{
if (hasMulliganed) return;
if (deck?.Divers.Contains(card.Name) == true) return; // Cannot mulligan divers
private CardData? LookupCard(string cardName)
if (mulliganSelected.Contains(card))
mulliganSelected.Remove(card);
else
mulliganSelected.Add(card);
}
private void Mulligan()
{
if (deck == null || hasMulliganed || mulliganSelected.Count == 0) return;
var random = new Random();
var mainDeck = deck.Cards.Select(LookupCard).Where(c => c != null).Cast<CardData>().ToList();
var nonDiversInDeck = mainDeck.Where(c => !deck.Divers.Contains(c.Name)).ToList();
// Filter out cards currently in hand
var currentHandNames = hand.Select(c => c.Name).ToList();
var availablePool = nonDiversInDeck.Where(c => !currentHandNames.Contains(c.Name)).ToList();
var cardsToReplace = mulliganSelected.ToList();
foreach (var oldCard in cardsToReplace)
{
return CardLookup.GetValueOrDefault(cardName);
}
private void SelectCard(CardData? card)
{
selectedCard = card;
}
private void CloseDetail()
{
selectedCard = null;
}
private List<CardData> hand = [];
private HashSet<CardData> mulliganSelected = [];
private bool hasMulliganed = false;
private bool canMulligan => mulliganSelected.Count > 0 && !hasMulliganed;
private void DrawInitialHand()
{
if (deck == null) return;
hand.Clear();
mulliganSelected.Clear();
hasMulliganed = false;
var random = new Random();
// 1. Get Divers
var divers = deck.Divers.Select(LookupCard).Where(c => c != null).Cast<CardData>().ToList();
// 2. Get Main Deck (excluding divers if they are in the main list, though usually they are separate in data)
var mainDeck = deck.Cards.Select(LookupCard).Where(c => c != null).Cast<CardData>().ToList();
// Add 3 random non-diver cards from main deck
var nonDivers = mainDeck.Where(c => !deck.Divers.Contains(c.Name)).OrderBy(_ => random.Next()).Take(4).ToList();
hand.AddRange(nonDivers);
// Final shuffle of hand for display
hand = hand.OrderBy(_ => random.Next()).ToList();
// Add 2 random divers
if (divers.Count > 0)
if (availablePool.Count > 0)
{
var newHand = divers.Take(2).ToList();
newHand.AddRange(hand);
var newCardIdx = random.Next(availablePool.Count);
var newCard = availablePool[newCardIdx];
hand = newHand;
}
}
private void ToggleMulliganSelection(CardData card)
{
if (hasMulliganed) return;
if (deck?.Divers.Contains(card.Name) == true) return; // Cannot mulligan divers
if (mulliganSelected.Contains(card))
mulliganSelected.Remove(card);
else
mulliganSelected.Add(card);
}
private void Mulligan()
{
if (deck == null || hasMulliganed || mulliganSelected.Count == 0) return;
var random = new Random();
var mainDeck = deck.Cards.Select(LookupCard).Where(c => c != null).Cast<CardData>().ToList();
var nonDiversInDeck = mainDeck.Where(c => !deck.Divers.Contains(c.Name)).ToList();
// Filter out cards currently in hand
var currentHandNames = hand.Select(c => c.Name).ToList();
var availablePool = nonDiversInDeck.Where(c => !currentHandNames.Contains(c.Name)).ToList();
var cardsToReplace = mulliganSelected.ToList();
foreach (var oldCard in cardsToReplace)
{
if (availablePool.Count > 0)
var handIdx = hand.IndexOf(oldCard);
if (handIdx != -1)
{
var newCardIdx = random.Next(availablePool.Count);
var newCard = availablePool[newCardIdx];
var handIdx = hand.IndexOf(oldCard);
if (handIdx != -1)
{
hand[handIdx] = newCard;
}
availablePool.RemoveAt(newCardIdx);
hand[handIdx] = newCard;
}
}
mulliganSelected.Clear();
hasMulliganed = true;
availablePool.RemoveAt(newCardIdx);
}
}
private List<(int Cost, int Count)> GetManaDistribution()
mulliganSelected.Clear();
hasMulliganed = true;
}
private List<(int Cost, int Count)> GetManaDistribution()
{
if (deck == null) return [];
var counts = new Dictionary<int, int>();
for (var i = 0; i <= 10; i++) counts[i] = 0;
foreach (var cardName in deck.Cards)
{
if (deck == null) return [];
var counts = new Dictionary<int, int>();
for (var i = 0; i <= 10; i++) counts[i] = 0;
foreach (var cardName in deck.Cards)
var card = LookupCard(cardName);
if (card?.Cost != null)
{
var card = LookupCard(cardName);
if (card?.Cost != null)
{
var cost = card.Cost.Value;
if (cost >= 10) cost = 10;
counts[cost]++;
}
var cost = card.Cost.Value;
if (cost >= 10) cost = 10;
counts[cost]++;
}
return counts.OrderBy(x => x.Key).Select(x => (x.Key, x.Value)).ToList();
}
return counts.OrderBy(x => x.Key).Select(x => (x.Key, x.Value)).ToList();
}
}
+1 -1
View File
@@ -131,7 +131,7 @@
gap: 1rem;
align-items: flex-start;
}
.hand-card-wrapper {
width: 120px;
}
+1 -1
View File
@@ -89,7 +89,7 @@
if (!string.IsNullOrEmpty(s1))
query = query.Where(d => d.Syndicates.Contains(s1, StringComparer.OrdinalIgnoreCase));
if (!string.IsNullOrEmpty(s2))
query = query.Where(d => d.Syndicates.Contains(s2, StringComparer.OrdinalIgnoreCase));
+3 -3
View File
@@ -1,5 +1,5 @@
@inject Shared.Services.CardRenderingService CardRenderer
@inject Shared.Services.CardPreviewService PreviewService
@inject CardRenderingService CardRenderer
@inject CardPreviewService PreviewService
@namespace Shared.Pages
@page "/docs"
@@ -88,7 +88,7 @@
@if (key == "description")
{
<div class="doc-field-desc">
@CardRenderer.RenderDescription(val, _ => {}, doc => PreviewService.SelectDoc(doc), PreviewService)
@CardRenderer.RenderDescription(val, _ => { }, doc => PreviewService.SelectDoc(doc), PreviewService)
</div>
}
else
+4 -3
View File
@@ -1,5 +1,4 @@
@page "/syndicate-pairings"
@using Model
@inject NavigationManager Nav
<PageTitle>Syndicates - Chrono</PageTitle>
@@ -36,14 +35,15 @@
</div>
@code {
private record Pairing(string S1, string S2);
private IEnumerable<Pairing> GetPairings()
{
var syndicates = new[] { "Singularity", "Phasetide", "Silence", "Lifeblood", "Sungrace", "Splintergleam" };
for (int i = 0; i < syndicates.Length; i++)
for (var i = 0; i < syndicates.Length; i++)
{
for (int j = i + 1; j < syndicates.Length; j++)
for (var j = i + 1; j < syndicates.Length; j++)
{
yield return new Pairing(syndicates[i], syndicates[j]);
}
@@ -54,4 +54,5 @@
{
Nav.NavigateTo($"/decks?s1={pairing.S1}&s2={pairing.S2}");
}
}
+1 -1
View File
@@ -31,7 +31,7 @@
</div>
<Shared.Components.SyndicateDialog Doc="selectedDoc" OnClose="CloseSyndicate" OnNavigate="HandleNavigate"/>
<Shared.Components.CardDetailModal Card="selectedCard" IsVisible="@(selectedCard != null)" OnClose="CloseCard" />
<Shared.Components.CardDetailModal Card="selectedCard" IsVisible="@(selectedCard != null)" OnClose="CloseCard"/>
<Shared.Components.DocDetailModal Doc="selectedGenericDoc" OnClose="CloseGenericDoc"/>
@code {
@@ -1,14 +1,15 @@
using System.Text.RegularExpressions;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Web;
using Model;
namespace Shared.Services;
public class CardRenderingService
{
private readonly List<string> _allTerms;
private readonly Dictionary<string, CardData> _cardLookup;
private readonly Dictionary<string, DocData> _docLookup;
private readonly List<string> _allTerms;
private readonly Regex _termRegex;
public CardRenderingService()
@@ -39,7 +40,8 @@ public class CardRenderingService
_termRegex = new Regex(pattern, RegexOptions.IgnoreCase | RegexOptions.Compiled);
}
public RenderFragment RenderDescription(string description, Action<CardData?> onCardClick, Action<DocData?>? onDocClick = null, CardPreviewService? previewService = null)
public RenderFragment RenderDescription(string description, Action<CardData?> onCardClick,
Action<DocData?>? onDocClick = null, CardPreviewService? previewService = null)
{
return builder =>
{
@@ -69,12 +71,13 @@ public class CardRenderingService
{
builder.AddAttribute(sequence++, "onclick",
EventCallback.Factory.Create(this, () => onCardClick(card)));
if (previewService != null)
{
builder.AddAttribute(sequence++, "onmouseenter",
EventCallback.Factory.Create<Microsoft.AspNetCore.Components.Web.MouseEventArgs>(this, e => previewService.Show(card, e.ClientX, e.ClientY)));
builder.AddAttribute(sequence++, "onmouseleave",
builder.AddAttribute(sequence++, "onmouseenter",
EventCallback.Factory.Create<MouseEventArgs>(this,
e => previewService.Show(card, e.ClientX, e.ClientY)));
builder.AddAttribute(sequence++, "onmouseleave",
EventCallback.Factory.Create(this, () => previewService.Hide()));
}
}