From bdf9a9519d292dfd945b6715f0e0a151d7399499 Mon Sep 17 00:00:00 2001 From: "boris.klyus" Date: Sun, 26 Jul 2026 13:46:55 +0100 Subject: [PATCH 1/6] enhance limited deck builder --- .../components/draft/LimitedDeckBuilder.tsx | 80 +++++++++++++- client/src/components/draft/ManaCurve.tsx | 100 ++++++++++++------ client/src/i18n/locales/de/draft.json | 4 + client/src/i18n/locales/en/draft.json | 4 + client/src/i18n/locales/es/draft.json | 4 + client/src/i18n/locales/fr/draft.json | 4 + client/src/i18n/locales/it/draft.json | 4 + client/src/i18n/locales/pl/draft.json | 4 + client/src/i18n/locales/pt/draft.json | 4 + crates/draft-core/src/validation.rs | 28 ++--- 10 files changed, 181 insertions(+), 55 deletions(-) diff --git a/client/src/components/draft/LimitedDeckBuilder.tsx b/client/src/components/draft/LimitedDeckBuilder.tsx index 0c61e3cf8f..7a523cf407 100644 --- a/client/src/components/draft/LimitedDeckBuilder.tsx +++ b/client/src/components/draft/LimitedDeckBuilder.tsx @@ -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"; @@ -176,6 +176,12 @@ function computeRemainingPool( return remaining; } +function primaryTypeLineIsLand(typeLine: string): boolean { + const primary = typeLine.split(" // ")[0] ?? typeLine; + const coreTypes = primary.split("—")[0] ?? primary; + return coreTypes.split(/\s+/).some((word) => word.toLowerCase() === "land"); +} + // ── Main component ────────────────────────────────────────────────────── interface LimitedDeckBuilderProps { @@ -219,6 +225,7 @@ export function LimitedDeckBuilder({ const submitDeck = onSubmitDeck ?? quickSubmitDeck; const [hoveredCard, setHoveredCard] = useState(null); + const [copied, setCopied] = useState(false); const pool = useMemo(() => view?.pool ?? [], [view?.pool]); @@ -232,18 +239,69 @@ export function LimitedDeckBuilder({ [pool, mainDeck], ); - const totalLands = useMemo( + const landNameSet = useMemo( + () => + new Set( + pool + .filter((c) => primaryTypeLineIsLand(c.type_line)) + .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[] => { + const countMap = new Map(); + for (const name of names) { + countMap.set(name, (countMap.get(name) ?? 0) + 1); + } + const lines: string[] = []; + for (const [name, count] of countMap) { + lines.push(`${count} ${name}`); + } + for (const [name, count] of Object.entries(extra)) { + if (count > 0) lines.push(`${count} ${name}`); + } + return lines; + }; + + const sideboardNames = remainingPool.map((c) => c.name); + const deckLines = toLines(mainDeck, landCounts); + const sideboardLines = toLines(sideboardNames); + + 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 ( @@ -254,7 +312,7 @@ export function LimitedDeckBuilder({ mobileLayout="compact" onDismiss={() => setHoveredCard(null)} /> - +
{/* Left column: Pool + Main Deck */} @@ -344,7 +402,7 @@ export function LimitedDeckBuilder({ {/* Mana curve */}
- +
{/* Actions */} @@ -358,6 +416,18 @@ export function LimitedDeckBuilder({ {t("limitedDeck.suggestDeck")} )} +