👁️
at main 202 lines 4.8 kB view raw
1import { getPreset } from "@/lib/deck-validation/presets"; 2 3export interface FormatGroup { 4 label: string; 5 formats: { value: string; label: string }[]; 6} 7 8export type CommanderType = 9 | "commander" 10 | "oathbreaker" 11 | "brawl" 12 | "pauper" 13 | null; 14 15export interface FormatInfo { 16 deckSize: number | "variable"; 17 singleton: boolean; 18 commanderType: CommanderType; 19 hasSignatureSpell: boolean; 20 hasSideboard: boolean; 21 tagline: string; 22 isCube: boolean; 23} 24 25/** 26 * Short taglines describing each format's identity. 27 * Focused on what makes formats unique, not rules details. 28 */ 29const FORMAT_TAGLINES: Record<string, string> = { 30 // Constructed 31 standard: "Rotating · Recent sets", 32 pioneer: "Non-rotating · 2012+", 33 modern: "Non-rotating · 2003+", 34 legacy: "Eternal · All sets", 35 vintage: "Eternal · Power allowed", 36 pauper: "Commons only", 37 38 // Commander 39 commander: "Multiplayer · Casual", 40 duel: "1v1 · Competitive", 41 paupercommander: "Commons + uncommon commander", 42 predh: "Pre-2020 cards only", 43 oathbreaker: "Planeswalker + signature spell", 44 45 // Brawl 46 brawl: "Arena · Historic card pool", 47 standardbrawl: "Arena · Standard card pool", 48 49 // Arena 50 historic: "Arena · Non-rotating", 51 timeless: "Arena · No bans", 52 alchemy: "Arena · Digital cards", 53 gladiator: "Arena · 100-card singleton", 54 55 // Retro 56 premodern: "1995–2003 cards", 57 oldschool: "1993–1994 cards", 58 59 // Limited 60 draft: "40-card · Drafted cards + basic lands", 61 62 // Other 63 penny: "Budget · Rotating cheapest", 64 cube: "Draft · Custom card pool", 65 66 // Casual 67 kitchentable: "Anything goes", 68}; 69 70/** 71 * Get stable format info that won't change with rules updates. 72 * Used for UI display (deck size badges, singleton indicators, etc.) 73 */ 74export function getFormatInfo(format: string): FormatInfo { 75 // Special case: Cube 76 if (format === "cube") { 77 return { 78 deckSize: "variable", 79 singleton: true, 80 commanderType: null, 81 hasSignatureSpell: false, 82 hasSideboard: false, 83 tagline: FORMAT_TAGLINES.cube, 84 isCube: true, 85 }; 86 } 87 88 const preset = getPreset(format); 89 if (!preset) { 90 return { 91 deckSize: 60, 92 singleton: false, 93 commanderType: null, 94 hasSignatureSpell: false, 95 hasSideboard: true, 96 tagline: "", 97 isCube: false, 98 }; 99 } 100 101 const { rules, config } = preset; 102 const rulesSet = new Set(rules); 103 104 const deckSize = config.deckSize ?? config.minDeckSize ?? 60; 105 const singleton = rulesSet.has("singleton"); 106 const hasSideboard = (config.sideboardSize ?? 0) > 0; 107 const hasSignatureSpell = rulesSet.has("signatureSpell"); 108 109 // Determine commander type 110 let commanderType: CommanderType = null; 111 if (rulesSet.has("commanderPlaneswalker")) { 112 commanderType = "oathbreaker"; 113 } else if (rulesSet.has("commanderRequired")) { 114 if (format === "brawl" || format === "standardbrawl") { 115 commanderType = "brawl"; 116 } else if (format === "paupercommander") { 117 commanderType = "pauper"; 118 } else { 119 commanderType = "commander"; 120 } 121 } 122 123 return { 124 deckSize, 125 singleton, 126 commanderType, 127 hasSignatureSpell, 128 hasSideboard, 129 tagline: FORMAT_TAGLINES[format] ?? "", 130 isCube: false, 131 }; 132} 133 134export const FORMAT_GROUPS: FormatGroup[] = [ 135 { 136 label: "Constructed", 137 formats: [ 138 { value: "standard", label: "Standard" }, 139 { value: "pioneer", label: "Pioneer" }, 140 { value: "modern", label: "Modern" }, 141 { value: "legacy", label: "Legacy" }, 142 { value: "vintage", label: "Vintage" }, 143 { value: "pauper", label: "Pauper" }, 144 ], 145 }, 146 { 147 label: "Commander", 148 formats: [ 149 { value: "commander", label: "Commander" }, 150 { value: "duel", label: "Duel Commander" }, 151 { value: "paupercommander", label: "Pauper Commander" }, 152 { value: "predh", label: "PreDH" }, 153 { value: "oathbreaker", label: "Oathbreaker" }, 154 ], 155 }, 156 { 157 label: "Brawl", 158 formats: [ 159 { value: "brawl", label: "Brawl" }, 160 { value: "standardbrawl", label: "Standard Brawl" }, 161 ], 162 }, 163 { 164 label: "Arena", 165 formats: [ 166 { value: "historic", label: "Historic" }, 167 { value: "timeless", label: "Timeless" }, 168 { value: "alchemy", label: "Alchemy" }, 169 { value: "gladiator", label: "Gladiator" }, 170 ], 171 }, 172 { 173 label: "Retro", 174 formats: [ 175 { value: "premodern", label: "Premodern" }, 176 { value: "oldschool", label: "Old School" }, 177 ], 178 }, 179 { 180 label: "Limited", 181 formats: [{ value: "draft", label: "Draft" }], 182 }, 183 { 184 label: "Other", 185 formats: [ 186 { value: "penny", label: "Penny Dreadful" }, 187 { value: "cube", label: "Cube" }, 188 { value: "kitchentable", label: "Kitchen Table" }, 189 ], 190 }, 191]; 192 193const FORMAT_DISPLAY_NAMES: Record<string, string> = Object.fromEntries( 194 FORMAT_GROUPS.flatMap((group) => 195 group.formats.map((fmt) => [fmt.value, fmt.label]), 196 ), 197); 198 199export function formatDisplayName(format: string | undefined): string { 200 if (!format) return ""; 201 return FORMAT_DISPLAY_NAMES[format] ?? format; 202}