Skip to content
Open
13 changes: 13 additions & 0 deletions data/Mega Evolution/Ascended Heroes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}

Expand Down
2 changes: 1 addition & 1 deletion data/Mega Evolution/Ascended Heroes/265.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ const card: Card = {
},

illustrator: "Saboteri",
rarity: "Ultra Rare",
rarity: "Mega Attack Rare",
category: "Pokemon",
hp: 310,
types: ["Water"],
Expand Down
2 changes: 1 addition & 1 deletion data/Mega Evolution/Ascended Heroes/266.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ const card: Card = {
},

illustrator: "DOM",
rarity: "Ultra Rare",
rarity: "Mega Attack Rare",
category: "Pokemon",
hp: 350,
types: ["Lightning"],
Expand Down
2 changes: 1 addition & 1 deletion data/Mega Evolution/Ascended Heroes/267.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ const card: Card = {
},

illustrator: "DOM",
rarity: "Ultra Rare",
rarity: "Mega Attack Rare",
category: "Pokemon",
hp: 270,
types: ["Psychic"],
Expand Down
2 changes: 1 addition & 1 deletion data/Mega Evolution/Ascended Heroes/268.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ const card: Card = {
},

illustrator: "Taiga Kasai",
rarity: "Ultra Rare",
rarity: "Mega Attack Rare",
category: "Pokemon",
hp: 250,
types: ["Fighting"],
Expand Down
2 changes: 1 addition & 1 deletion data/Mega Evolution/Ascended Heroes/269.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ const card: Card = {
},

illustrator: "Taiga Kasai",
rarity: "Ultra Rare",
rarity: "Mega Attack Rare",
category: "Pokemon",
hp: 350,
types: ["Darkness"],
Expand Down
2 changes: 1 addition & 1 deletion data/Mega Evolution/Ascended Heroes/270.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ const card: Card = {
},

illustrator: "Taiga Kasai",
rarity: "Ultra Rare",
rarity: "Mega Attack Rare",
category: "Pokemon",
hp: 330,
types: ["Darkness"],
Expand Down
2 changes: 1 addition & 1 deletion data/Mega Evolution/Ascended Heroes/271.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ const card: Card = {
},

illustrator: "DOM",
rarity: "Ultra Rare",
rarity: "Mega Attack Rare",
category: "Pokemon",
hp: 370,
types: ["Dragon"],
Expand Down
55 changes: 54 additions & 1 deletion interfaces.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<variant_detailed> {
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<Record<Card['rarity'], PullRateValue>>
specialVariants?: SpecialVariantPullRate[]
}

export interface Set {
id: string
name: Languages
Expand All @@ -160,8 +198,22 @@ export interface Set {

boosters?: Record<string, {
name: Languages<string>

/**
* 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<ISODate>

thirdParty?: {
Expand Down Expand Up @@ -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'

Expand Down
17 changes: 17 additions & 0 deletions meta/definitions/api.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, PullRateValue>
specialVariants?: Array<{ rate: PullRateValue } & Record<string, unknown>>
}

export interface SetResume {
Expand Down Expand Up @@ -148,7 +156,9 @@ export interface Set extends SetResume {
cardmarket?: number
tcgplayer?: number
}
pullRates?: PullRates
}

export interface CardResume {
id: string;
localId: string;
Expand Down Expand Up @@ -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
*
Expand Down
3 changes: 2 additions & 1 deletion meta/translations/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
3 changes: 2 additions & 1 deletion meta/translations/es.json
Original file line number Diff line number Diff line change
Expand Up @@ -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é",
Expand Down
3 changes: 2 additions & 1 deletion meta/translations/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -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é",
Expand Down
3 changes: 2 additions & 1 deletion meta/translations/it.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
3 changes: 2 additions & 1 deletion meta/translations/pt.json
Original file line number Diff line number Diff line change
Expand Up @@ -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ê",
Expand Down
12 changes: 7 additions & 5 deletions server/compiler/utils/cardUtil.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string | undefined> {
try {
Expand Down Expand Up @@ -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) ?
Expand All @@ -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),
Expand Down
43 changes: 43 additions & 0 deletions server/compiler/utils/pull-rates.ts
Original file line number Diff line number Diff line change
@@ -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<variant_detailed>
): boolean {
return (Object.keys(rule) as Array<keyof variant_detailed>).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<variant_detailed>)
})
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]
}
24 changes: 14 additions & 10 deletions server/compiler/utils/setUtil.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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]
}
Expand All @@ -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;
}

Expand All @@ -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))),
Expand All @@ -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,
}
}