diff --git a/data/Mega Evolution/Ascended Heroes.ts b/data/Mega Evolution/Ascended Heroes.ts index cdc339c3b6..300d260787 100644 --- a/data/Mega Evolution/Ascended Heroes.ts +++ b/data/Mega Evolution/Ascended Heroes.ts @@ -30,6 +30,19 @@ const set: Set = { thirdParty: { cardmarket: 6395, tcgplayer: 24541 + }, + + pullRates: { + rarities: { + 'Double rare': { display: '1 in 5', percent: 20 }, + 'Illustration rare': { display: '1 in 9', percent: 11.11 }, + 'Ultra Rare': { display: '1 in 21', percent: 4.76 }, + 'Mega Attack Rare': { display: '1 in 29', percent: 3.45 }, + 'Special illustration rare': { display: '1 in 70', percent: 1.43 }, + 'Mega Hyper Rare': { display: '1 in 540', percent: 0.19 }, + }, + // specialVariants omitted — reverse foils are fixed guaranteed slots + // per card, not probabilistic pull rates } } diff --git a/data/Mega Evolution/Ascended Heroes/265.ts b/data/Mega Evolution/Ascended Heroes/265.ts index d958383e5f..18b7d6fffb 100644 --- a/data/Mega Evolution/Ascended Heroes/265.ts +++ b/data/Mega Evolution/Ascended Heroes/265.ts @@ -23,7 +23,7 @@ const card: Card = { }, illustrator: "Saboteri", - rarity: "Ultra Rare", + rarity: "Mega Attack Rare", category: "Pokemon", hp: 310, types: ["Water"], diff --git a/data/Mega Evolution/Ascended Heroes/266.ts b/data/Mega Evolution/Ascended Heroes/266.ts index e214459805..4db70494b6 100644 --- a/data/Mega Evolution/Ascended Heroes/266.ts +++ b/data/Mega Evolution/Ascended Heroes/266.ts @@ -24,7 +24,7 @@ const card: Card = { }, illustrator: "DOM", - rarity: "Ultra Rare", + rarity: "Mega Attack Rare", category: "Pokemon", hp: 350, types: ["Lightning"], diff --git a/data/Mega Evolution/Ascended Heroes/267.ts b/data/Mega Evolution/Ascended Heroes/267.ts index 25bfcf7487..86a2408b64 100644 --- a/data/Mega Evolution/Ascended Heroes/267.ts +++ b/data/Mega Evolution/Ascended Heroes/267.ts @@ -15,7 +15,7 @@ const card: Card = { }, illustrator: "DOM", - rarity: "Ultra Rare", + rarity: "Mega Attack Rare", category: "Pokemon", hp: 270, types: ["Psychic"], diff --git a/data/Mega Evolution/Ascended Heroes/268.ts b/data/Mega Evolution/Ascended Heroes/268.ts index 331b65ba7a..228f2bc6b3 100644 --- a/data/Mega Evolution/Ascended Heroes/268.ts +++ b/data/Mega Evolution/Ascended Heroes/268.ts @@ -15,7 +15,7 @@ const card: Card = { }, illustrator: "Taiga Kasai", - rarity: "Ultra Rare", + rarity: "Mega Attack Rare", category: "Pokemon", hp: 250, types: ["Fighting"], diff --git a/data/Mega Evolution/Ascended Heroes/269.ts b/data/Mega Evolution/Ascended Heroes/269.ts index dbb47cbc7d..1b8e9c358a 100644 --- a/data/Mega Evolution/Ascended Heroes/269.ts +++ b/data/Mega Evolution/Ascended Heroes/269.ts @@ -24,7 +24,7 @@ const card: Card = { }, illustrator: "Taiga Kasai", - rarity: "Ultra Rare", + rarity: "Mega Attack Rare", category: "Pokemon", hp: 350, types: ["Darkness"], diff --git a/data/Mega Evolution/Ascended Heroes/270.ts b/data/Mega Evolution/Ascended Heroes/270.ts index 9757c6d85a..2baa7e1352 100644 --- a/data/Mega Evolution/Ascended Heroes/270.ts +++ b/data/Mega Evolution/Ascended Heroes/270.ts @@ -24,7 +24,7 @@ const card: Card = { }, illustrator: "Taiga Kasai", - rarity: "Ultra Rare", + rarity: "Mega Attack Rare", category: "Pokemon", hp: 330, types: ["Darkness"], diff --git a/data/Mega Evolution/Ascended Heroes/271.ts b/data/Mega Evolution/Ascended Heroes/271.ts index 8837afde21..9b30f5c290 100644 --- a/data/Mega Evolution/Ascended Heroes/271.ts +++ b/data/Mega Evolution/Ascended Heroes/271.ts @@ -23,7 +23,7 @@ const card: Card = { }, illustrator: "DOM", - rarity: "Ultra Rare", + rarity: "Mega Attack Rare", category: "Pokemon", hp: 370, types: ["Dragon"], diff --git a/interfaces.d.ts b/interfaces.d.ts index ee5c21b950..6e0d4295c0 100644 --- a/interfaces.d.ts +++ b/interfaces.d.ts @@ -144,6 +144,44 @@ export type Types = 'Colorless' | 'Darkness' | 'Dragon' | type ISODate = `${number}-${number}-${number}` +/** + * Represents a pull rate value. + * Can be a simple display string (e.g. '1 in 8') or an object + * with a required decimal percent for programmatic use. + * + * @example '1 in 8' + * @example { display: '1 in 8', percent: 12.5 } +*/ +export type PullRateValue = | string | { + display: string + percent: number +} + +/** + * A pull-rate rule for a specific card variant. + * + * Any variant fields present on this object are used to match against a + * card's detailed variant entry. Fields that are not present are ignored. + * + * @example { type: 'reverse', foil: 'masterball', rate: { display: '1 in 40', percent: 2.5 } } +*/ +export interface SpecialVariantPullRate extends Partial { + rate: PullRateValue +} + +/** + * Pull rates for a set or booster. + * - `rarities` maps Card rarity strings directly to pull rates + * - `specialVariants` defines pull rates for specific variant treatments + * + * Pull rates are derived from these and exposed on each entry in + * variants_detailed — they are never authored manually on individual cards. + */ +export interface PullRates { + rarities?: Partial> + specialVariants?: SpecialVariantPullRate[] +} + export interface Set { id: string name: Languages @@ -160,8 +198,22 @@ export interface Set { boosters?: Record + + /** + * Optional pull rate override for this specific booster. + * Only define when this booster has meaningfully different + * odds from the set-level pullRates. + */ + pullRates?: PullRates }> + /** + * The pull rate for a given rarity or variant. + * Can be a simple display string (e.g. '1 in 8') or an object + * with an optional decimal percent for programmatic use. + */ + pullRates?: PullRates + releaseDate: ISODate | Languages thirdParty?: { @@ -231,7 +283,8 @@ export interface Card { 'Shiny rare VMAX' | 'Special illustration rare' | 'Ultra Rare' | 'Uncommon' // Black White rare | 'Black White Rare' - | 'Mega Hyper Rare' + | 'Mega Hyper Rare' + | 'Mega Attack Rare' // Pokémon TCG Pocket Rarities | 'One Diamond' | 'Two Diamond' | 'Three Diamond' | 'Four Diamond' | 'One Star' | 'Two Star' | 'Three Star' | 'Crown' | 'One Shiny' | 'Two Shiny' diff --git a/meta/definitions/api.d.ts b/meta/definitions/api.d.ts index d50764874b..37e093c37a 100644 --- a/meta/definitions/api.d.ts +++ b/meta/definitions/api.d.ts @@ -68,6 +68,14 @@ interface variant_detailed { tcgplayer?: number } variantId: string + pullRate?: string | { display: string; percent?: number } +} + +export type PullRateValue = string | { display: string; percent: number } + +export interface PullRates { + rarities?: Record + specialVariants?: Array<{ rate: PullRateValue } & Record> } export interface SetResume { @@ -148,7 +156,9 @@ export interface Set extends SetResume { cardmarket?: number tcgplayer?: number } + pullRates?: PullRates } + export interface CardResume { id: string; localId: string; @@ -183,6 +193,13 @@ export interface Card extends CardResume { * - Secret Rare */ rarity: string; + + /** + * Derived pull rate for the card's rarity. + * Populated from the parent set's pullRates.rarities. + */ + pullRate?: PullRateValue + /** * Card Category * diff --git a/meta/translations/de.json b/meta/translations/de.json index 085a7c92e7..37622ac386 100644 --- a/meta/translations/de.json +++ b/meta/translations/de.json @@ -57,7 +57,8 @@ "Two Shiny": "Deux Chromatique", "Three Shiny": "Un Chromatique", "Black White Rare": "Schwarz-Weiß Selten", - "Mega Hyper Rare": "Mega Hyper Selten" + "Mega Hyper Rare": "Mega Hyper Selten", + "Mega Attack Rare": "Mega Angriff Selten" }, "stage": { "Baby": "Baby", diff --git a/meta/translations/es.json b/meta/translations/es.json index 4d46aacd39..5ac531444b 100644 --- a/meta/translations/es.json +++ b/meta/translations/es.json @@ -57,7 +57,8 @@ "Two Shiny": "Deux Chromatique", "Three Shiny": "Un Chromatique", "Black White Rare": "Rara Blanco y Negro", - "Mega Hyper Rare": "Mega Hiper Raro" + "Mega Hyper Rare": "Mega Hiper Raro", + "Mega Attack Rare": "Mega Ataque Rara" }, "stage": { "Baby": "Bebé", diff --git a/meta/translations/fr.json b/meta/translations/fr.json index e069e129e1..3cf3a39dd9 100644 --- a/meta/translations/fr.json +++ b/meta/translations/fr.json @@ -56,7 +56,8 @@ "Two Shiny": "Deux Chromatiques", "Three Shiny": "Trois Chromatiques", "Black White Rare": "Rare Noir Blanc", - "Mega Hyper Rare": "Méga Hyper Rare" + "Mega Hyper Rare": "Méga Hyper Rare", + "Mega Attack Rare": "Mega Attaque Rare" }, "stage": { "Baby": "Bébé", diff --git a/meta/translations/it.json b/meta/translations/it.json index b18f3292f3..15909ac27a 100644 --- a/meta/translations/it.json +++ b/meta/translations/it.json @@ -57,7 +57,8 @@ "Two Shiny": "Deux Chromatique", "Three Shiny": "Un Chromatique", "Black White Rare": "Rara Bianco e Nero", - "Mega Hyper Rare": "Mega Iper Raro" + "Mega Hyper Rare": "Mega Iper Raro", + "Mega Attack Rare": "Mega Attacco Rara" }, "stage": { "Baby": "Bambino", diff --git a/meta/translations/pt.json b/meta/translations/pt.json index 75dcbbd0fb..b445ce0b12 100644 --- a/meta/translations/pt.json +++ b/meta/translations/pt.json @@ -56,7 +56,8 @@ "Two Shiny": "Deux Chromatique", "Three Shiny": "Un Chromatique", "Black White Rare": "Rara Preto e Branco", - "Mega Hyper Rare": "Mega Hiper Raro" + "Mega Hyper Rare": "Mega Hiper Raro", + "Mega Attack Rare": "Mega Ataque Rara" }, "stage": { "Baby": "Bebê", diff --git a/server/compiler/utils/cardUtil.ts b/server/compiler/utils/cardUtil.ts index a67127b873..2537797a3d 100644 --- a/server/compiler/utils/cardUtil.ts +++ b/server/compiler/utils/cardUtil.ts @@ -7,6 +7,7 @@ import translate from './translationUtil' import { DB_PATH, cardIsLegal, fetchRemoteFile, getDataFolder, getLastEdit, resolveText, smartGlob } from './util' import { objectMap, objectPick } from '@dzeio/object-util' import { formatVariant, variantToIdentifier } from "./variantUtil.ts"; +import { deriveCardPullRate, deriveVariantPullRate } from './pull-rates.ts' export async function getCardPictures(cardId: string, card: Card, lang: SupportedLanguages): Promise { try { @@ -90,6 +91,7 @@ export async function cardToCardSingle(localId: string, card: Card, lang: Suppor name: resolveText(card.name, lang) as string, rarity: translate('rarity', card.rarity, lang) as any, + pullRate: deriveCardPullRate(card), set: await setToSetSimple(card.set, lang), variants : Array.isArray(card.variants) ? @@ -102,13 +104,13 @@ export async function cardToCardSingle(localId: string, card: Card, lang: Suppor }, variants_detailed: Array.isArray(card.variants) - ? await Promise.all(card.variants.map(async (variant, index) => { - const variantId = variantToIdentifier(variant); - let formattedVariant = formatVariant(variant,lang) - + ? await Promise.all(card.variants.map(async (variant) => { + const variantId = variantToIdentifier(variant) + const formattedVariant = formatVariant(variant, lang) return { ...formattedVariant, - variantId + variantId, + pullRate: deriveVariantPullRate(variant, card) } as ApiVariantDetailed })) : variantsToVariantsDetailed(card.variants, lang), diff --git a/server/compiler/utils/pull-rates.ts b/server/compiler/utils/pull-rates.ts new file mode 100644 index 0000000000..16554c7991 --- /dev/null +++ b/server/compiler/utils/pull-rates.ts @@ -0,0 +1,43 @@ +import { Card, PullRateValue, variant_detailed } from '../../../interfaces' + +/** + * Returns true if all rules defined on 'rule' match the corresponding + * fields on 'variant'. Fields not present on 'rule' are ignored + */ +function variantMatchesRule( + variant: variant_detailed, + rule: Partial +): boolean { + return (Object.keys(rule) as Array).every( + (key) => variant[key] === rule[key] + ) +} + +/** + * Derives the pull rate for a specific variant from its parent set's pullRates. + * Returns undefined if no matching rule exists. + */ +export function deriveVariantPullRate( + variant: variant_detailed, + card: Card +): PullRateValue | undefined { + const pullRates = card.set.pullRates + if (!pullRates?.specialVariants) return undefined + + const rule = pullRates.specialVariants.find((r) => { + const { rate, ...match } = r + return variantMatchesRule(variant, match as Partial) + }) + return rule?.rate +} + +/** + * Derives the pull rate for a card's rarity from its parent set's pullRates. + * Returns undefined if no match exists. + */ +export function deriveCardPullRate(card: Card): PullRateValue | undefined { + const pullRates = card.set.pullRates + if (!pullRates?.rarities || !card.rarity) return undefined + + return pullRates.rarities[card.rarity] +} \ No newline at end of file diff --git a/server/compiler/utils/setUtil.ts b/server/compiler/utils/setUtil.ts index 81856c8dc8..3316448099 100644 --- a/server/compiler/utils/setUtil.ts +++ b/server/compiler/utils/setUtil.ts @@ -56,10 +56,7 @@ export async function getSetPictures(set: Set, lang: SupportedLanguages): Promis const file = await fetchRemoteFile('https://assets.tcgdex.net/datas.json') const logoExists = file[lang]?.[set.serie.id]?.[set.id]?.logo ? `https://assets.tcgdex.net/${lang}/${set.serie.id}/${set.id}/logo` : undefined const symbolExists = file.univ?.[set.serie.id]?.[set.id]?.symbol ? `https://assets.tcgdex.net/univ/${set.serie.id}/${set.id}/symbol` : undefined - return [ - logoExists, - symbolExists - ] + return [logoExists, symbolExists] } catch { return [undefined, undefined] } @@ -81,7 +78,7 @@ export async function setToSetSimple(set: Set, lang: SupportedLanguages): Promis } function getVariantCountForType(card: Card, type: 'normal' | 'reverse' | 'holo' | 'firstEdition'): number { - if( card.variants === undefined || card.variants === null) { + if (card.variants === undefined || card.variants === null) { return 0; } @@ -103,11 +100,11 @@ export async function setToSetSingle(set: Set, lang: SupportedLanguages): Promis const pics = await getSetPictures(set, lang) return { cardCount: { - firstEd: cards.reduce((count, card) => count + getVariantCountForType(card[1],"firstEdition"), 0), - holo: cards.reduce((count, card) => count + getVariantCountForType(card[1],"holo"), 0), - normal: cards.reduce((count, card) => count + getVariantCountForType(card[1],"normal"), 0), + firstEd: cards.reduce((count, card) => count + getVariantCountForType(card[1], "firstEdition"), 0), + holo: cards.reduce((count, card) => count + getVariantCountForType(card[1], "holo"), 0), + normal: cards.reduce((count, card) => count + getVariantCountForType(card[1], "normal"), 0), official: set.cardCount.official, - reverse: cards.reduce((count, card) => count + getVariantCountForType(card[1],"reverse"), 0), + reverse: cards.reduce((count, card) => count + getVariantCountForType(card[1], "reverse"), 0), total: Math.max(set.cardCount.official, cards.length) }, cards: await Promise.all(cards.map(([id, card]) => cardToCardSimple(id, card, lang))), @@ -134,6 +131,13 @@ export async function setToSetSingle(set: Set, lang: SupportedLanguages): Promis name: resolveText(booster.name, lang), // images will be coming soon... })) : undefined, - thirdParty: set.thirdParty + pullRates: set.pullRates ? { + rarities: set.pullRates.rarities, + specialVariants: set.pullRates.specialVariants?.map(({ rate, ...rest }) => ({ + ...rest, + rate + })) + } : undefined, + thirdParty: set.thirdParty, } }