// Dominion — card definitions (pure data, no Phaser). // // Each card's `frame` is its index in the `dominion-cards` spritesheet // (270×390 per cell, art in the top ~60%; the title/icon band is drawn at // runtime). The art sheet must be laid out in this exact frame order: // // 0 copper 1 silver 2 gold 3 estate 4 duchy 5 province 6 curse // 7 cellar … 32 artisan (Kingdom cards, in the order declared below) // // `plus` holds the vanilla bonuses (+cards/+actions/+buys/+coins) which the // engine auto-applies and the UI renders as the icon summary. Everything // beyond vanilla lives in DominionLogic's CARD_EFFECTS registry. const def = (id, frame, cost, types, extra = {}) => ({ id, name: extra.name ?? titleCase(id), frame, cost, types, plus: { cards: 0, actions: 0, buys: 0, coins: 0, ...(extra.plus ?? {}) }, coin: extra.coin, // treasure value vp: extra.vp, // fixed victory points (Gardens is dynamic → handled in logic) text: extra.text ?? '', }); function titleCase(id) { return id.replace(/(^|\s)\w/g, (c) => c.toUpperCase()); } // ── Card table ──────────────────────────────────────────────────────────────── const LIST = [ // Base cards def('copper', 0, 0, ['treasure'], { coin: 1, text: 'Worth 1 Coin.' }), def('silver', 1, 3, ['treasure'], { coin: 2, text: 'Worth 2 Coins.' }), def('gold', 2, 6, ['treasure'], { coin: 3, text: 'Worth 3 Coins.' }), def('estate', 3, 2, ['victory'], { vp: 1, text: 'Worth 1 Victory Point.' }), def('duchy', 4, 5, ['victory'], { vp: 3, text: 'Worth 3 Victory Points.' }), def('province', 5, 8, ['victory'], { vp: 6, text: 'Worth 6 Victory Points.' }), def('curse', 6, 0, ['curse'], { vp: -1, text: 'Worth -1 Victory Point.' }), // Kingdom cards (2nd-edition base set) def('cellar', 7, 2, ['action'], { name: 'Cellar', plus: { actions: 1 }, text: '+1 Action. Discard any number of cards, then draw that many.' }), def('chapel', 8, 2, ['action'], { name: 'Chapel', text: 'Trash up to 4 cards from your hand.' }), def('moat', 9, 2, ['action', 'reaction'],{ name: 'Moat', plus: { cards: 2 }, text: '+2 Cards. When another player plays an Attack card, you may first reveal this from your hand, to be unaffected by it.' }), def('harbinger', 10, 3, ['action'], { name: 'Harbinger', plus: { cards: 1, actions: 1 }, text: '+1 Card, +1 Action. Look through your discard pile. You may put a card from it onto your deck.' }), def('merchant', 11, 3, ['action'], { name: 'Merchant', plus: { cards: 1, actions: 1 }, text: '+1 Card, +1 Action. The first time you play a Silver this turn, +1 Coin.' }), def('vassal', 12, 3, ['action'], { name: 'Vassal', plus: { coins: 2 }, text: '+2 Coins. Discard the top card of your deck. If it is an Action card, you may play it.' }), def('village', 13, 3, ['action'], { name: 'Village', plus: { cards: 1, actions: 2 }, text: '+1 Card, +2 Actions.' }), def('workshop', 14, 3, ['action'], { name: 'Workshop', text: 'Gain a card costing up to 4 Coins.' }), def('bureaucrat', 15, 4, ['action', 'attack'], { name: 'Bureaucrat', text: 'Gain a Silver onto your deck. Each other player reveals a Victory card from their hand and puts it onto their deck (or reveals a hand with no Victory cards).' }), def('gardens', 16, 4, ['victory'], { name: 'Gardens', text: 'Worth 1 Victory Point per 10 cards you have (round down).' }), def('militia', 17, 4, ['action', 'attack'], { name: 'Militia', plus: { coins: 2 }, text: '+2 Coins. Each other player discards down to 3 cards in hand.' }), def('moneylender',18, 4, ['action'], { name: 'Moneylender', text: 'You may trash a Copper from your hand for +3 Coins.' }), def('poacher', 19, 4, ['action'], { name: 'Poacher', plus: { cards: 1, actions: 1, coins: 1 }, text: '+1 Card, +1 Action, +1 Coin. Discard a card per empty Supply pile.' }), def('remodel', 20, 4, ['action'], { name: 'Remodel', text: 'Trash a card from your hand. Gain a card costing up to 2 Coins more than it.' }), def('smithy', 21, 4, ['action'], { name: 'Smithy', plus: { cards: 3 }, text: '+3 Cards.' }), def('throneroom', 22, 4, ['action'], { name: 'Throne Room', text: 'You may play an Action card from your hand twice.' }), def('bandit', 23, 5, ['action', 'attack'], { name: 'Bandit', text: 'Gain a Gold. Each other player reveals the top 2 cards of their deck, trashes a revealed Treasure other than Copper, and discards the rest.' }), def('councilroom',24, 5, ['action'], { name: 'Council Room', plus: { cards: 4, buys: 1 }, text: '+4 Cards, +1 Buy. Each other player draws a card.' }), def('festival', 25, 5, ['action'], { name: 'Festival', plus: { actions: 2, buys: 1, coins: 2 }, text: '+2 Actions, +1 Buy, +2 Coins.' }), def('laboratory', 26, 5, ['action'], { name: 'Laboratory', plus: { cards: 2, actions: 1 }, text: '+2 Cards, +1 Action.' }), def('library', 27, 5, ['action'], { name: 'Library', text: 'Draw until you have 7 cards in hand, skipping any Action cards you choose to; set those aside, discarding them afterwards.' }), def('market', 28, 5, ['action'], { name: 'Market', plus: { cards: 1, actions: 1, buys: 1, coins: 1 }, text: '+1 Card, +1 Action, +1 Buy, +1 Coin.' }), def('mine', 29, 5, ['action'], { name: 'Mine', text: 'You may trash a Treasure from your hand. Gain a Treasure to your hand costing up to 3 Coins more than it.' }), def('sentry', 30, 5, ['action'], { name: 'Sentry', plus: { cards: 1, actions: 1 }, text: '+1 Card, +1 Action. Look at the top 2 cards of your deck. Trash and/or discard any number of them. Put the rest back on top in any order.' }), def('witch', 31, 5, ['action', 'attack'], { name: 'Witch', plus: { cards: 2 }, text: '+2 Cards. Each other player gains a Curse.' }), def('artisan', 32, 6, ['action'], { name: 'Artisan', plus: {}, text: 'Gain a card to your hand costing up to 5 Coins. Put a card from your hand onto your deck.' }), ]; export const CARDS = Object.fromEntries(LIST.map((c) => [c.id, c])); export function getCard(id) { const c = CARDS[id]; if (!c) throw new Error(`Unknown Dominion card: ${id}`); return c; } export const BASE_TREASURES = ['copper', 'silver', 'gold']; export const BASE_VICTORY = ['estate', 'duchy', 'province']; // The 26 Kingdom cards, in frame order — the pool Random mode draws from. export const KINGDOM_POOL = LIST.filter((c) => c.frame >= 7).map((c) => c.id); // Standard mode: the official "First Game" recommended Kingdom. export const FIRST_GAME = [ 'cellar', 'market', 'merchant', 'militia', 'mine', 'moat', 'remodel', 'smithy', 'village', 'workshop', ]; // Additional recommended kingdoms from the 2nd-edition rulebook. export const SIZE_DISTORTION = [ 'artisan', 'bandit', 'bureaucrat', 'gardens', 'harbinger', 'laboratory', 'moneylender', 'throneroom', 'village', 'witch', ]; export const DECK_TOP = [ 'artisan', 'bureaucrat', 'councilroom', 'festival', 'harbinger', 'laboratory', 'poacher', 'sentry', 'vassal', 'village', ]; export const SILVER_AND_GOLD = [ 'artisan', 'bandit', 'councilroom', 'mine', 'moneylender', 'poacher', 'remodel', 'smithy', 'throneroom', 'vassal', ]; export const HELPFUL_ACTIONS = [ 'cellar', 'councilroom', 'festival', 'harbinger', 'laboratory', 'library', 'merchant', 'militia', 'moat', 'sentry', ]; export const KINGDOM_PRESETS = { 'standard': FIRST_GAME, 'size-distortion': SIZE_DISTORTION, 'deck-top': DECK_TOP, 'silver-gold': SILVER_AND_GOLD, 'helpful-actions': HELPFUL_ACTIONS, }; export function hasType(card, type) { return card.types.includes(type); } export function isType(id, type) { return getCard(id).types.includes(type); } // Pick 10 distinct Kingdom ids from the pool using the supplied rng (0..1). export function chooseRandomKingdom(rand) { const pool = KINGDOM_POOL.slice(); for (let i = pool.length - 1; i > 0; i--) { const j = Math.floor(rand() * (i + 1)); [pool[i], pool[j]] = [pool[j], pool[i]]; } return pool.slice(0, 10).sort((a, b) => getCard(a).cost - getCard(b).cost); } export function kingdomFor(deckMode, rand) { if (deckMode === 'random') return chooseRandomKingdom(rand); const preset = KINGDOM_PRESETS[deckMode] ?? FIRST_GAME; return preset.slice().sort((a, b) => getCard(a).cost - getCard(b).cost); }