fertig-classic-games/public/src/games/dominion/DominionCards.js

135 lines
8.6 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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);
}