Files
6d486f49 ffaab59585 ...
2026-09-09 17:19:03 -04:00

720 lines
36 KiB
Plaintext

@page "/deck-builder"
@page "/deck-builder/{DeckId:guid}"
@using Model
@using Web.Services
@using Shared.Components
@inject UserDecksService UserDecksService
@inject AuthService AuthService
@inject NavigationManager Navigation
@inject NetworkStatusService NetworkStatus
@implements IDisposable
<PageTitle>Chrono CCG - @(string.IsNullOrEmpty(deck.Name) ? "Deck Builder" : deck.Name)</PageTitle>
<HeadContent>
<meta name="description" content="Build and customize your Chrono CCG deck with 40 cards, 2 diver cards, notes, and season tracking." />
</HeadContent>
<div class="deck-builder-page container-fluid px-2 px-lg-4 py-3">
@if (!AuthService.IsLoggedIn)
{
<div class="auth-required-card glass-panel p-4 p-md-5 text-center my-5 mx-auto" style="max-width: 580px;">
<div class="mb-3">
<i class="bi bi-shield-lock-fill auth-lock-icon"></i>
</div>
<h2 class="fw-bold text-white mb-2">Authentication Required</h2>
<p class="text-secondary mb-4 px-md-3">
Log in to create, customize, and save your Chrono CCG decks to your cloud profile.
</p>
<div>
<a href="login" class="btn save-btn px-4 py-2">
<i class="bi bi-box-arrow-in-right"></i> Log In to Continue
</a>
</div>
</div>
}
else
{
<!-- Header / Controls Bar -->
<div class="builder-header-bar glass-panel p-3 p-md-4 mb-3">
<div class="row align-items-center g-3">
<div class="col-12 col-md-4 col-lg-3">
<label class="field-label">
<i class="bi bi-pencil-square text-primary"></i> Deck Name
</label>
<input type="text" class="form-control deck-name-input input-glow"
placeholder="Enter Deck Name..." @bind="deck.Name" @bind:event="oninput" />
</div>
<div class="col-6 col-md-3 col-lg-2">
<label class="field-label">
<i class="bi bi-trophy text-warning"></i> Season
</label>
<input type="text" class="form-control deck-season-input input-glow"
placeholder="e.g. Season 1" @bind="deck.Season" @bind:event="oninput" />
</div>
<div class="col-6 col-md-5 col-lg-4 d-flex align-items-center status-pill-group pt-md-3">
<div>
<span class="card-count @(deck.Cards.Count == 40 ? "count-complete" : deck.Cards.Count > 40 ? "count-overflow" : "count-progress")"
title="Main deck must have exactly 40 cards">
<i class="bi @(deck.Cards.Count == 40 ? "bi-check-circle-fill" : "bi-layers-fill")"></i>
@deck.Cards.Count / 40
</span>
</div>
<div>
<span class="validity-badge @(deck.IsValid ? "valid" : "invalid")" title="@deck.ValidationMessage">
<i class="bi @(deck.IsValid ? "bi-shield-check" : "bi-exclamation-triangle-fill")"></i>
@(deck.IsValid ? "Valid" : deck.ValidationMessage)
</span>
</div>
</div>
<div class="col-12 col-lg-3 d-flex align-items-center justify-content-lg-end gap-2 pt-lg-3">
<button class="save-btn" @onclick="SaveDeckAsync" disabled="@isSaving">
@if (isSaving)
{
<span class="spinner-border spinner-border-sm" role="status"></span>
<span>Saving...</span>
}
else
{
<i class="bi bi-floppy2-fill"></i>
<span>Save Deck</span>
}
</button>
<a href="my-decks" class="btn-ghost-secondary">
<i class="bi bi-collection-play"></i> My Decks
</a>
</div>
</div>
<!-- Notes and Last Updated Row -->
<div class="row mt-3 pt-3 border-top border-secondary border-opacity-25 align-items-center g-2">
<div class="col-12 col-md-8">
<div class="input-group input-group-sm">
<span class="input-group-text bg-dark border-secondary border-opacity-50 text-secondary">
<i class="bi bi-sticky-fill text-warning me-1"></i> Strategy Notes
</span>
<input type="text" class="form-control deck-notes-input input-glow"
placeholder="Add strategy, combo ideas, or match-up notes..." @bind="deck.Notes" @bind:event="oninput" />
</div>
</div>
<div class="col-12 col-md-4 text-md-end text-secondary small deck-updated-at">
<i class="bi bi-clock-history me-1 text-primary"></i> Last updated: @deck.UpdatedAt.ToLocalTime().ToString("MMM dd, yyyy HH:mm")
</div>
</div>
</div>
@if (!string.IsNullOrEmpty(errorMessage))
{
<div class="alert alert-danger py-2 px-3 small mb-3 alert-dismissible fade show glass-panel border-danger text-danger-emphasis d-flex align-items-center" role="alert">
<i class="bi bi-exclamation-triangle-fill me-2 fs-5 text-danger"></i>
<div class="flex-grow-1">@errorMessage</div>
<button type="button" class="btn-close" @onclick="() => errorMessage = null"></button>
</div>
}
<!-- Builder Main Workspace: Browser on Left, Deck List & Analytics on Right -->
<div class="row g-3">
<!-- Left: Card Browser -->
<div class="col-12 col-lg-8">
<div class="glass-panel p-3 p-md-4 h-100 d-flex flex-column">
<div class="d-flex flex-wrap align-items-center justify-content-between gap-2 mb-3">
<h5 class="fw-bold text-white mb-0 d-flex align-items-center gap-2">
<i class="bi bi-grid-3x3-gap-fill text-primary"></i> Card Browser
</h5>
<div class="small text-secondary">
<span class="text-white-50">Showing @FilteredBrowserCards.Count() of @allCards.Count(c => (c.Category is "Agent" or "Spell") && !c.IsImmortalized) cards</span>
<span class="mx-1">•</span>
<span>Click card to add (max 3)</span>
</div>
</div>
<!-- Search and Filters -->
<div class="row g-2 mb-3 filter-search-group">
<div class="col-12 col-md-4">
<div class="input-group input-group-sm">
<span class="input-group-text"><i class="bi bi-search"></i></span>
<input type="text" class="form-control input-glow"
placeholder="Search name, text, archetype..." @bind="searchFilter" @bind:event="oninput" />
@if (!string.IsNullOrEmpty(searchFilter))
{
<button class="btn btn-outline-secondary" @onclick="() => searchFilter = string.Empty"><i class="bi bi-x"></i></button>
}
</div>
</div>
<div class="col-6 col-md-3">
<select class="form-select form-select-sm filter-select" @bind="categoryFilter">
<option value="">All Categories</option>
<option value="Agent">Agents</option>
<option value="Spell">Spells</option>
</select>
</div>
<div class="col-6 col-md-3">
<select class="form-select form-select-sm filter-select" @bind="syndicateFilter">
<option value="">All Syndicates</option>
@foreach (var syn in syndicates)
{
<option value="@syn">@syn</option>
}
</select>
</div>
<div class="col-12 col-md-2 d-flex gap-1">
<select class="form-select form-select-sm filter-select flex-grow-1" @bind="costFilter">
<option value="">Cost</option>
@for (int i = 0; i <= 13; i++)
{
<option value="@i.ToString()">Cost @i</option>
}
</select>
@if (!string.IsNullOrEmpty(searchFilter) || !string.IsNullOrEmpty(categoryFilter) || !string.IsNullOrEmpty(syndicateFilter) || !string.IsNullOrEmpty(costFilter))
{
<button class="btn btn-filter-clear" @onclick="ResetFilters" title="Reset all filters">
<i class="bi bi-arrow-counterclockwise"></i>
</button>
}
</div>
</div>
<!-- Card Grid -->
<div class="card-browser-grid flex-grow-1 overflow-auto">
@foreach (var card in FilteredBrowserCards)
{
var countInDeck = deck.Cards.Count(c => c == card.Name);
var isDiver = deck.Divers.Contains(card.Name);
var isMaxed = countInDeck >= 3 || deck.Cards.Count >= 40 || isDiver;
var catClass = card.IsAgent ? "card-agent" : card.IsSpell ? "card-spell" : card.IsImmortalized ? "card-immortalized" : "";
<div class="browser-card @catClass @(countInDeck > 0 ? "in-deck" : "") @(isMaxed ? "max-copies" : "cursor-pointer")"
@onclick="() => AddCardToDeck(card)">
<div class="card-img-wrapper">
<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=%22140%22 height=%22190%22><rect fill=%22%231a1a36%22 width=%22140%22 height=%22190%22/><text fill=%22%238888aa%22 font-size=%2212%22 x=%2270%22 y=%2295%22 text-anchor=%22middle%22 dominant-baseline=%22middle%22>Card</text></svg>'" />
@if (card.Cost.HasValue)
{
<div class="cost-gem" title="Energy Cost: @card.Cost">
@card.Cost
</div>
}
@if (countInDeck > 0)
{
<div class="deck-count-badge @(countInDeck == 1 ? "count-1" : countInDeck == 2 ? "count-2" : "count-3")">
@(countInDeck == 3 ? "3 MAX" : $"{countInDeck}/3")
</div>
}
@if (isDiver)
{
<div class="diver-ribbon">
<i class="bi bi-shuffle"></i> DIVER
</div>
}
</div>
<div class="card-mini-info">
<div class="card-title-text" title="@card.Name">@card.Name</div>
<div class="card-stats-row">
<span class="card-cat-label @(card.IsAgent ? "cat-agent" : card.IsSpell ? "cat-spell" : "cat-immortalized")">
@card.Category
</span>
@if (card.Attack.HasValue && card.Health.HasValue)
{
<span class="combat-stats-chip" title="Attack / Health">
<span class="atk-val"><i class="bi bi-suit-spade-fill"></i>@card.Attack</span>
<span class="text-secondary opacity-50">/</span>
<span class="hp-val"><i class="bi bi-shield-fill"></i>@card.Health</span>
</span>
}
else if (!string.IsNullOrEmpty(card.Syndicate))
{
<span class="d-inline-flex align-items-center gap-1" style="font-size: 0.68rem;">
<span class="syndicate-dot @GetSyndicateDotClass(card.Syndicate)"></span>
<span class="text-white-50 text-truncate" style="max-width: 60px;">@card.Syndicate</span>
</span>
}
</div>
</div>
</div>
}
</div>
</div>
</div>
<!-- Right: Current Deck, Diver Slots, and Mana Curve -->
<div class="col-12 col-lg-4">
<div class="glass-panel p-3 p-md-4 h-100 d-flex flex-column gap-3">
<!-- Diver Slots Section -->
<div class="divers-section">
<div class="d-flex align-items-center justify-content-between mb-2">
<h6 class="fw-bold text-white mb-0 d-flex align-items-center gap-2">
<i class="bi bi-shuffle text-warning"></i> Diver Cards (2 Required)
</h6>
<span class="badge @(deck.Divers.Count == 2 && deck.Divers[0] != deck.Divers[1] && !deck.Cards.Contains(deck.Divers[0]) && !deck.Cards.Contains(deck.Divers[1]) ? "bg-success" : "bg-secondary")">
@deck.Divers.Count / 2
</span>
</div>
<div class="row g-2">
<!-- Diver Slot 0 -->
<div class="col-6">
<div class="diver-slot @(deck.Divers.Count > 0 && !string.IsNullOrEmpty(deck.Divers[0]) ? "occupied-slot" : "empty-slot")"
@onclick="() => OpenDiverPicker(0)">
@if (deck.Divers.Count > 0 && !string.IsNullOrEmpty(deck.Divers[0]))
{
<div class="d-flex justify-content-between align-items-center mb-1">
<span class="diver-badge-gold">Diver 1</span>
<button type="button" class="diver-remove-btn" title="Remove Diver"
@onclick:stopPropagation="true" @onclick="() => RemoveDiver(0)">
<i class="bi bi-x-circle-fill"></i>
</button>
</div>
<div class="deck-row-title" title="@deck.Divers[0]">
@deck.Divers[0]
</div>
}
else
{
<div class="text-center py-2 text-secondary small">
<i class="bi bi-plus-circle d-block mb-1 text-warning fs-5"></i>
<span class="text-white-50">Select Diver 1</span>
</div>
}
</div>
</div>
<!-- Diver Slot 1 -->
<div class="col-6">
<div class="diver-slot @(deck.Divers.Count > 1 && !string.IsNullOrEmpty(deck.Divers[1]) ? "occupied-slot" : "empty-slot")"
@onclick="() => OpenDiverPicker(1)">
@if (deck.Divers.Count > 1 && !string.IsNullOrEmpty(deck.Divers[1]))
{
<div class="d-flex justify-content-between align-items-center mb-1">
<span class="diver-badge-gold">Diver 2</span>
<button type="button" class="diver-remove-btn" title="Remove Diver"
@onclick:stopPropagation="true" @onclick="() => RemoveDiver(1)">
<i class="bi bi-x-circle-fill"></i>
</button>
</div>
<div class="deck-row-title" title="@deck.Divers[1]">
@deck.Divers[1]
</div>
}
else
{
<div class="text-center py-2 text-secondary small">
<i class="bi bi-plus-circle d-block mb-1 text-warning fs-5"></i>
<span class="text-white-50">Select Diver 2</span>
</div>
}
</div>
</div>
</div>
</div>
<!-- Deck Quick Stats Breakdown -->
@if (deck.Cards.Any())
{
<div class="deck-stats-bar">
<div class="d-flex flex-wrap align-items-center justify-content-between gap-1 mb-2">
<div class="d-flex flex-wrap gap-1">
<span class="stats-chip chip-agent" title="Agent Cards">
<i class="bi bi-person-badge-fill"></i> @AgentsCount
</span>
<span class="stats-chip chip-spell" title="Spell Cards">
<i class="bi bi-magic"></i> @SpellsCount
</span>
<span class="stats-chip chip-immortalized" title="Immortalized Cards">
<i class="bi bi-stars"></i> @ImmortalizedCount
</span>
</div>
@if (DeckSyndicates.Any())
{
<div class="d-flex align-items-center gap-1" title="Included Syndicates">
@foreach (var syn in DeckSyndicates)
{
<span class="syndicate-dot @GetSyndicateDotClass(syn)" title="@syn"></span>
}
</div>
}
</div>
<div class="mana-curve-wrapper pt-1">
<ManaCurve Distribution="GetManaDistribution()" MaxCostLabel="8" />
</div>
</div>
}
<!-- Main Deck Cards List Section -->
<div class="deck-cards-section flex-grow-1 d-flex flex-column">
<div class="d-flex align-items-center justify-content-between mb-2">
<h6 class="fw-bold text-white mb-0 d-flex align-items-center gap-2">
<i class="bi bi-stack text-primary"></i> Main Deck
</h6>
<div class="d-flex align-items-center gap-2">
<button class="btn btn-sm btn-outline-danger py-0 px-2" @onclick="ClearDeck" disabled="@(!deck.Cards.Any())" title="Clear all cards">
<i class="bi bi-trash"></i> Clear
</button>
<span class="badge bg-secondary">
@GroupedDeckCards.Count unique
</span>
</div>
</div>
@if (!deck.Cards.Any())
{
<div class="empty-deck-box flex-grow-1 d-flex flex-column align-items-center justify-content-center">
<i class="bi bi-inboxes empty-deck-icon"></i>
<h6 class="text-white mb-1">Deck is Empty</h6>
<p class="text-secondary small mb-0">Click cards in the browser to add them to your deck.</p>
</div>
}
else
{
<div class="deck-card-list flex-grow-1 overflow-auto">
@foreach (var group in GroupedDeckCards)
{
var cardData = allCards.FirstOrDefault(c => c.Name == group.Name);
var rowCatClass = cardData?.IsAgent == true ? "row-agent" : cardData?.IsSpell == true ? "row-spell" : cardData?.IsImmortalized == true ? "row-immortalized" : "";
<div class="deck-row-item @rowCatClass" @key="group.Name">
<div class="d-flex align-items-center gap-2 text-truncate pe-2">
@if (cardData?.Cost.HasValue == true)
{
<span class="mini-cost-pill">@cardData.Cost</span>
}
<span class="deck-row-title" title="@group.Name">@group.Name</span>
</div>
<div class="quantity-stepper">
<button class="stepper-btn" @onclick="() => RemoveCardCopy(group.Name)" title="Remove copy">-</button>
<span class="qty-pill @(group.Count == 1 ? "qty-1" : group.Count == 2 ? "qty-2" : "qty-3")">@group.Count</span>
<button class="stepper-btn"
@onclick="() => AddCardCopy(group.Name)"
disabled="@(group.Count >= 3 || deck.Cards.Count >= 40)"
title="Add copy">+</button>
</div>
</div>
}
</div>
}
</div>
</div>
</div>
</div>
<!-- Diver Picker Modal Dialog -->
@if (showDiverPicker)
{
<div class="modal fade show d-block" tabindex="-1" style="background: rgba(0,0,0,0.8); z-index: 1050;">
<div class="modal-dialog modal-lg modal-dialog-scrollable modal-dialog-centered">
<div class="modal-content modal-glass text-white">
<div class="modal-header border-secondary border-opacity-25 pb-3">
<h5 class="modal-title fw-bold d-flex align-items-center gap-2">
<i class="bi bi-shuffle text-warning"></i> Select Diver @(activeDiverSlot + 1)
</h5>
<button type="button" class="btn-close btn-close-white" @onclick="CloseDiverPicker"></button>
</div>
<div class="modal-body py-3">
<div class="alert alert-info py-2 px-3 small mb-3 border-info border-opacity-25 bg-info bg-opacity-10 text-info">
<i class="bi bi-info-circle-fill me-1"></i> Diver cards must be unique and cannot be cards that are currently in your main deck.
</div>
<div class="input-group mb-3 filter-search-group">
<span class="input-group-text"><i class="bi bi-search"></i></span>
<input type="text" class="form-control input-glow"
placeholder="Filter eligible diver cards..." @bind="diverSearch" @bind:event="oninput" />
@if (!string.IsNullOrEmpty(diverSearch))
{
<button class="btn btn-outline-secondary" @onclick="() => diverSearch = string.Empty"><i class="bi bi-x"></i></button>
}
</div>
<div class="diver-picker-grid overflow-auto">
@{
var availableDivers = GetAvailableDivers();
}
@if (!availableDivers.Any())
{
<div class="w-100 text-center py-4 text-secondary">
No eligible diver cards match your filter.
</div>
}
else
{
@foreach (var card in availableDivers)
{
<div class="diver-picker-card"
@onclick="() => SelectDiver(card.Name)">
<div class="fw-bold small text-truncate text-white mb-1" title="@card.Name">@card.Name</div>
<div class="text-secondary small d-flex justify-content-between align-items-center" style="font-size: 0.75rem;">
<span class="card-cat-label @(card.IsAgent ? "cat-agent" : card.IsSpell ? "cat-spell" : "cat-immortalized")">@card.Category</span>
<span class="mini-cost-pill" style="width: 18px; height: 18px; font-size: 0.65rem;">@(card.Cost?.ToString() ?? "—")</span>
</div>
@if (!string.IsNullOrEmpty(card.Syndicate))
{
<div class="d-flex align-items-center gap-1 mt-1" style="font-size: 0.7rem;">
<span class="syndicate-dot @GetSyndicateDotClass(card.Syndicate)" style="width: 7px; height: 7px;"></span>
<span class="text-white-50 text-truncate">@card.Syndicate</span>
</div>
}
</div>
}
}
</div>
</div>
<div class="modal-footer border-secondary border-opacity-25 pt-2">
<button type="button" class="btn btn-secondary px-3" @onclick="CloseDiverPicker">Cancel</button>
</div>
</div>
</div>
</div>
}
<!-- Save Toast Alert -->
@if (showSaveToast)
{
<div class="save-toast position-fixed bottom-0 end-0 m-4 shadow-lg d-flex align-items-center gap-3"
role="alert">
<i class="bi bi-check-circle-fill fs-4"></i>
<div>
<div class="fw-bold">Saved!</div>
<div class="small opacity-75">Deck updated successfully in database.</div>
</div>
<button type="button" class="btn-close btn-close-white ms-2" @onclick="() => showSaveToast = false"></button>
</div>
}
}
</div>
@code {
[Parameter] public Guid? DeckId { get; set; }
private UserDeck deck = new() { Name = "New Deck", Season = "Season 1" };
private List<CardData> allCards = [];
private List<string> syndicates = [];
private string searchFilter = "";
private string categoryFilter = "";
private string syndicateFilter = "";
private string costFilter = "";
private string diverSearch = "";
private bool showDiverPicker;
private int activeDiverSlot = 0;
private bool isSaving;
private bool showSaveToast;
private string? errorMessage;
private record GroupedCard(string Name, int Count);
private List<GroupedCard> GroupedDeckCards => deck.Cards
.GroupBy(c => c)
.Select(g => new GroupedCard(g.Key, g.Count()))
.OrderBy(g => g.Name)
.ToList();
private IEnumerable<CardData> FilteredBrowserCards => allCards
.Where(c => (c.Category is "Agent" or "Spell") && !c.IsImmortalized)
.Where(c => string.IsNullOrWhiteSpace(searchFilter) || c.MatchesSearch(searchFilter))
.Where(c => string.IsNullOrEmpty(categoryFilter) || c.Category == categoryFilter)
.Where(c => string.IsNullOrEmpty(syndicateFilter) || c.Syndicate == syndicateFilter)
.Where(c => string.IsNullOrEmpty(costFilter) || c.Cost?.ToString() == costFilter)
.OrderBy(c => c.Cost ?? 99)
.ThenBy(c => c.Name);
private int AgentsCount => deck.Cards.Count(name => allCards.FirstOrDefault(c => c.Name == name)?.Category == "Agent");
private int SpellsCount => deck.Cards.Count(name => allCards.FirstOrDefault(c => c.Name == name)?.Category == "Spell");
private int ImmortalizedCount => deck.Cards.Count(name => allCards.FirstOrDefault(c => c.Name == name)?.Category == "Immortalized");
private List<string> DeckSyndicates => deck.Cards
.Select(name => allCards.FirstOrDefault(c => c.Name == name)?.Syndicate)
.Where(s => !string.IsNullOrEmpty(s))
.Distinct()
.ToList()!;
private List<(int Cost, int Count)> GetManaDistribution()
{
var costs = deck.Cards
.Select(name => allCards.FirstOrDefault(card => card.Name == name)?.Cost ?? 0)
.ToList();
var maxCost = 8;
return Enumerable.Range(0, maxCost + 1)
.Select(cost => (Cost: cost, Count: costs.Count(c => c == cost)))
.ToList();
}
private static string GetSyndicateDotClass(string? syndicate)
{
return syndicate?.ToLowerInvariant() switch
{
"singularity" => "syn-singularity",
"phasetide" => "syn-phasetide",
"silence" => "syn-silence",
"lifeblood" => "syn-lifeblood",
"sungrace" => "syn-sungrace",
"splintergleam" => "syn-splintergleam",
_ => ""
};
}
private void ResetFilters()
{
searchFilter = "";
categoryFilter = "";
syndicateFilter = "";
costFilter = "";
}
protected override async Task OnInitializedAsync()
{
allCards = CardDatabase.Cards;
syndicates = allCards
.Select(c => c.Syndicate)
.Where(s => !string.IsNullOrEmpty(s))
.Distinct()
.OrderBy(s => s)
.ToList()!;
AuthService.AuthStateChanged += OnAuthStateChanged;
await AuthService.InitializeAsync();
}
protected override async Task OnParametersSetAsync()
{
if (DeckId.HasValue && DeckId.Value != Guid.Empty)
{
if (deck.Id != DeckId.Value)
{
var loaded = await UserDecksService.GetDeckAsync(DeckId.Value);
if (loaded != null)
{
deck = loaded;
}
}
}
}
private void OnAuthStateChanged()
{
InvokeAsync(StateHasChanged);
}
private void AddCardToDeck(CardData card)
{
if (deck.Cards.Count >= 40) return;
if (deck.Cards.Count(c => c == card.Name) >= 3) return;
if (deck.Divers.Contains(card.Name)) return;
deck.Cards.Add(card.Name);
}
private void AddCardCopy(string cardName)
{
if (deck.Cards.Count >= 40) return;
if (deck.Cards.Count(c => c == cardName) >= 3) return;
if (deck.Divers.Contains(cardName)) return;
deck.Cards.Add(cardName);
}
private void RemoveCardCopy(string cardName)
{
deck.Cards.Remove(cardName);
}
private void ClearDeck()
{
deck.Cards.Clear();
}
private void OpenDiverPicker(int slot)
{
activeDiverSlot = slot;
diverSearch = "";
showDiverPicker = true;
}
private void CloseDiverPicker()
{
showDiverPicker = false;
}
private List<CardData> GetAvailableDivers()
{
var otherDiver = (activeDiverSlot == 0 && deck.Divers.Count > 1)
? deck.Divers[1]
: (activeDiverSlot == 1 && deck.Divers.Count > 0)
? deck.Divers[0]
: null;
return allCards
.Where(c => (c.Category is "Agent" or "Spell") && !c.IsImmortalized)
.Where(c => !deck.Cards.Contains(c.Name)) // Diver cannot be in deck
.Where(c => string.IsNullOrEmpty(otherDiver) || c.Name != otherDiver) // Divers must be unique
.Where(c => string.IsNullOrWhiteSpace(diverSearch) || c.MatchesSearch(diverSearch))
.OrderBy(c => c.Name)
.ToList();
}
private void SelectDiver(string cardName)
{
while (deck.Divers.Count <= activeDiverSlot)
{
deck.Divers.Add("");
}
deck.Divers[activeDiverSlot] = cardName;
deck.Divers = deck.Divers.Where(d => !string.IsNullOrEmpty(d)).ToList();
showDiverPicker = false;
}
private void RemoveDiver(int slot)
{
if (slot < deck.Divers.Count)
{
deck.Divers.RemoveAt(slot);
}
}
private async Task SaveDeckAsync()
{
if (string.IsNullOrWhiteSpace(deck.Name))
{
deck.Name = "Untitled Deck";
}
isSaving = true;
errorMessage = null;
try
{
var saved = await UserDecksService.SaveDeckAsync(deck);
if (saved != null)
{
var isNew = !DeckId.HasValue || DeckId.Value == Guid.Empty;
deck.Id = saved.Id;
deck.UpdatedAt = saved.UpdatedAt;
showSaveToast = true;
if (isNew || DeckId != saved.Id)
{
DeckId = saved.Id;
Navigation.NavigateTo($"/deck-builder/{saved.Id}");
}
}
else
{
errorMessage = "Failed to save deck to database.";
}
}
catch (Exception ex)
{
errorMessage = $"Error saving deck: {ex.Message}";
}
finally
{
isSaving = false;
}
}
public void Dispose()
{
AuthService.AuthStateChanged -= OnAuthStateChanged;
}
}