Skip to content
Open
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions client/src/adapter/draft-adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export interface DraftCardInstance {
colors: string[];
cmc: number;
type_line: string;
is_land: boolean;
}

// @sync-with: crates/draft-core/src/view.rs
Expand Down
75 changes: 70 additions & 5 deletions client/src/components/draft/LimitedDeckBuilder.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useMemo, useState } from "react";
import { useCallback, useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import { AnimatePresence, motion } from "framer-motion";

Expand Down Expand Up @@ -219,6 +219,7 @@ export function LimitedDeckBuilder({
const submitDeck = onSubmitDeck ?? quickSubmitDeck;

const [hoveredCard, setHoveredCard] = useState<CardHoverInfo | null>(null);
const [copied, setCopied] = useState(false);

const pool = useMemo(() => view?.pool ?? [], [view?.pool]);

Expand All @@ -232,18 +233,71 @@ export function LimitedDeckBuilder({
[pool, mainDeck],
);

const totalLands = useMemo(
const landNameSet = useMemo(
() =>
new Set(
pool
.filter((c) => c.is_land)
.map((c) => c.name),
),
[pool],
);

const mainDeckSpells = useMemo(
() => mainDeck.filter((name) => !landNameSet.has(name)),
[mainDeck, landNameSet],
);

const deckLandCount = mainDeck.length - mainDeckSpells.length;

const basicLands = useMemo(
() => Object.values(landCounts).reduce((sum, n) => sum + n, 0),
[landCounts],
);

const totalCards = mainDeck.length + totalLands;
const totalCards = mainDeck.length + basicLands;
const minDeckSize = view?.min_deck_size ?? 40;
const addableCards = view?.addable_cards?.length
? view.addable_cards
: BASIC_LANDS.map((land) => land.name);
const deckValid = totalCards >= minDeckSize;

const copyDeckList = useCallback(() => {
const toLines = (names: string[], extra: Record<string, number> = {}): string[] => {
const countMap = new Map<string, number>();
for (const name of names) {
countMap.set(name, (countMap.get(name) ?? 0) + 1);
}
for (const [name, count] of Object.entries(extra)) {
if (count > 0) {
countMap.set(name, (countMap.get(name) ?? 0) + count);
}
}
const lines: string[] = [];
for (const [name, count] of countMap) {
lines.push(`${count} ${name}`);
}
return lines;
};

const sideboardNames = remainingPool.map((c) => c.name);
const deckLines = toLines(mainDeck, landCounts);
const sideboardLines = toLines(sideboardNames);
Comment thread
klyusba marked this conversation as resolved.

const text = [
"Deck",
...deckLines,
"",
"Sideboard",
...sideboardLines,
].join("\n");

void navigator.clipboard.writeText(text).then(() => {
setCopied(true);
setTimeout(() => setCopied(false), 1800);
});
}, [mainDeck, landCounts, remainingPool]);

if (!view) return null;

return (
Expand All @@ -254,7 +308,7 @@ export function LimitedDeckBuilder({
mobileLayout="compact"
onDismiss={() => setHoveredCard(null)}
/>
<DeckStatus spells={mainDeck.length} lands={totalLands} min={minDeckSize} />
<DeckStatus spells={mainDeckSpells.length} lands={basicLands + deckLandCount} min={minDeckSize} />

<div className="flex min-h-0 flex-1 gap-6">
{/* Left column: Pool + Main Deck */}
Expand Down Expand Up @@ -344,7 +398,7 @@ export function LimitedDeckBuilder({

{/* Mana curve */}
<section>
<ManaCurve pool={pool} cards={mainDeck} />
<ManaCurve pool={pool} cards={mainDeckSpells} />
</section>

{/* Actions */}
Expand All @@ -358,6 +412,17 @@ export function LimitedDeckBuilder({
{t("limitedDeck.suggestDeck")}
</button>
)}
<button
type="button"
onClick={copyDeckList}
className={menuButtonClass({
tone: "neutral",
size: "sm",
className: "w-full",
})}
>
{copied ? t("limitedDeck.copied") : t("limitedDeck.copyList")}
Comment thread
klyusba marked this conversation as resolved.
</button>

<button
type="button"
Expand Down
102 changes: 100 additions & 2 deletions client/src/components/draft/__tests__/LimitedDeckBuilder.test.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { useState } from "react";
import { describe, expect, it, vi } from "vitest";
import { fireEvent, render, screen } from "@testing-library/react";
import { afterEach, describe, expect, it, vi } from "vitest";
import { cleanup, fireEvent, render, screen, waitFor } from "@testing-library/react";

import { LimitedDeckBuilder } from "../LimitedDeckBuilder";

Expand All @@ -19,6 +19,10 @@ vi.mock("../../../stores/draftStore", () => ({
}),
}));

afterEach(() => {
cleanup();
});

type BuilderView = NonNullable<NonNullable<Parameters<typeof LimitedDeckBuilder>[0]>["view"]>;

const TEST_VIEW: BuilderView = {
Expand All @@ -38,6 +42,7 @@ const TEST_VIEW: BuilderView = {
colors: ["U"],
cmc: 3,
type_line: "Creature - Drake",
is_land: false,
},
],
seats: [],
Expand All @@ -53,6 +58,24 @@ const TEST_VIEW: BuilderView = {
pairings: [],
};

const COPY_VIEW: BuilderView = {
...TEST_VIEW,
pool: [
...TEST_VIEW.pool,
{
instance_id: "card-2",
name: "Eager Cadet",
set_code: "dmu",
collector_number: "1",
rarity: "common",
colors: ["W"],
cmc: 1,
type_line: "Creature - Human Soldier",
is_land: false,
},
],
};

function Harness() {
const [mainDeck, setMainDeck] = useState<string[]>([]);

Expand Down Expand Up @@ -89,4 +112,79 @@ describe("LimitedDeckBuilder", () => {

expect(threeDropBucket).toHaveAttribute("aria-valuenow", "1");
});

it("uses domain land classification for deck accounting", () => {
const view: BuilderView = {
...TEST_VIEW,
pool: [
{
instance_id: "domain-land",
name: "Domain Land",
set_code: "tst",
collector_number: "100",
rarity: "rare",
colors: [],
cmc: 0,
type_line: "Creature",
is_land: true,
},
],
};

render(
<LimitedDeckBuilder
view={view}
mainDeck={["Domain Land"]}
landCounts={{}}
onAddToDeck={() => {}}
onRemoveFromDeck={() => {}}
onSetLandCount={() => {}}
onSubmitDeck={() => {}}
showSuggestions={false}
/>,
);

expect(screen.getByRole("meter", { name: "Mana value 0" })).toHaveAttribute(
"aria-valuenow",
"0",
);
});

it("copies the current deck list to the clipboard", async () => {
const writeText = vi.fn().mockResolvedValue(undefined);
Object.defineProperty(navigator, "clipboard", {
configurable: true,
value: { writeText },
});

render(
<LimitedDeckBuilder
view={COPY_VIEW}
mainDeck={["Wind Drake", "Island"]}
landCounts={{ Island: 2, Plains: 0, Forest: 1 }}
onAddToDeck={() => {}}
onRemoveFromDeck={() => {}}
onSetLandCount={() => {}}
onSubmitDeck={() => {}}
showSuggestions={false}
/>,
);

fireEvent.click(screen.getByRole("button", { name: "Copy Deck List" }));

expect(writeText).toHaveBeenCalledWith(
[
"Deck",
"1 Wind Drake",
"3 Island",
"1 Forest",
"",
"Sideboard",
"1 Eager Cadet",
].join("\n"),
);
await waitFor(() =>
expect(screen.getByRole("button", { name: "Copied!" })).toBeInTheDocument(),
);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ const view: DraftPlayerView = {
colors: ["R"],
cmc: 1,
type_line: "Instant",
is_land: false,
},
],
pool: [],
Expand Down
4 changes: 4 additions & 0 deletions client/src/i18n/locales/de/draft.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@
},
"manaCurve": {
"title": "Manakurve",
"allCards": "Alle",
"creatures": "Kreaturen",
"bucketLabel": "Manawert {{bucket}}"
},
"pickTimer": {
Expand Down Expand Up @@ -106,6 +108,8 @@
"addableCards": "Hinzufügbare Karten",
"autoLands": "Auto-Länder",
"suggestDeck": "Deck vorschlagen",
"copyList": "Deckliste kopieren",
"copied": "Kopiert!",
"submitDeck": "Deck einreichen",
"removeCard": "{{name}} entfernen",
"addCard": "{{name}} hinzufügen",
Expand Down
4 changes: 4 additions & 0 deletions client/src/i18n/locales/en/draft.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@
},
"manaCurve": {
"title": "Mana Curve",
"allCards": "All",
"creatures": "Creatures",
"bucketLabel": "Mana value {{bucket}}"
},
"pickTimer": {
Expand Down Expand Up @@ -106,6 +108,8 @@
"addableCards": "Addable Cards",
"autoLands": "Auto Lands",
"suggestDeck": "Suggest Deck",
"copyList": "Copy Deck List",
"copied": "Copied!",
"submitDeck": "Submit Deck",
"removeCard": "Remove {{name}}",
"addCard": "Add {{name}}",
Expand Down
4 changes: 4 additions & 0 deletions client/src/i18n/locales/es/draft.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@
},
"manaCurve": {
"title": "Curva de maná",
"allCards": "Todo",
"creatures": "Criaturas",
"bucketLabel": "Valor de maná {{bucket}}"
},
"pickTimer": {
Expand Down Expand Up @@ -106,6 +108,8 @@
"addableCards": "Cartas añadibles",
"autoLands": "Tierras automáticas",
"suggestDeck": "Sugerir mazo",
"copyList": "Copiar lista del mazo",
"copied": "¡Copiado!",
"submitDeck": "Enviar mazo",
"removeCard": "Quitar {{name}}",
"addCard": "Añadir {{name}}",
Expand Down
4 changes: 4 additions & 0 deletions client/src/i18n/locales/fr/draft.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@
},
"manaCurve": {
"title": "Courbe de mana",
"allCards": "Toutes",
"creatures": "Créatures",
"bucketLabel": "Valeur de mana {{bucket}}"
},
"pickTimer": {
Expand Down Expand Up @@ -106,6 +108,8 @@
"addableCards": "Cartes ajoutables",
"autoLands": "Terrains auto",
"suggestDeck": "Suggérer un deck",
"copyList": "Copier la liste du deck",
"copied": "Copié !",
"submitDeck": "Soumettre le deck",
"removeCard": "Retirer {{name}}",
"addCard": "Ajouter {{name}}",
Expand Down
4 changes: 4 additions & 0 deletions client/src/i18n/locales/it/draft.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@
},
"manaCurve": {
"title": "Curva del mana",
"allCards": "Tutte",
"creatures": "Creature",
"bucketLabel": "Valore di mana {{bucket}}"
},
"pickTimer": {
Expand Down Expand Up @@ -106,6 +108,8 @@
"addableCards": "Carte aggiungibili",
"autoLands": "Terre automatiche",
"suggestDeck": "Suggerisci mazzo",
"copyList": "Copia lista mazzo",
"copied": "Copiato!",
"submitDeck": "Invia mazzo",
"removeCard": "Rimuovi {{name}}",
"addCard": "Aggiungi {{name}}",
Expand Down
4 changes: 4 additions & 0 deletions client/src/i18n/locales/pl/draft.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@
},
"manaCurve": {
"title": "Krzywa many",
"allCards": "Wszystkie",
"creatures": "Stwory",
"bucketLabel": "Wartość many {{bucket}}"
},
"pickTimer": {
Expand Down Expand Up @@ -106,6 +108,8 @@
"addableCards": "Karty do dodania",
"autoLands": "Automatyczne ziemie",
"suggestDeck": "Zaproponuj talię",
"copyList": "Skopiuj listę talii",
"copied": "Skopiowano!",
"submitDeck": "Zatwierdź talię",
"removeCard": "Usuń {{name}}",
"addCard": "Dodaj {{name}}",
Expand Down
4 changes: 4 additions & 0 deletions client/src/i18n/locales/pt/draft.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@
},
"manaCurve": {
"title": "Curva de Mana",
"allCards": "Todas",
"creatures": "Criaturas",
"bucketLabel": "Valor de mana {{bucket}}"
},
"pickTimer": {
Expand Down Expand Up @@ -106,6 +108,8 @@
"addableCards": "Cartas Adicionáveis",
"autoLands": "Terrenos Automáticos",
"suggestDeck": "Sugerir Deck",
"copyList": "Copiar Lista do Deck",
"copied": "Copiado!",
"submitDeck": "Enviar Deck",
"removeCard": "Remover {{name}}",
"addCard": "Adicionar {{name}}",
Expand Down
Loading
Loading