135 lines
8.6 KiB
JavaScript
135 lines
8.6 KiB
JavaScript
// 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);
|
||
}
|