From 5642f4a00a39988464c874671df56be2563bbe76 Mon Sep 17 00:00:00 2001 From: Kevin Vacquier Date: Mon, 17 Nov 2025 14:25:23 +0100 Subject: [PATCH 1/2] =?UTF-8?q?feat:=20Add=20Trainer=20card=20legality=20c?= =?UTF-8?q?ache=20and=20optimize=20compilation=20order=20=20-=20Add=20Trai?= =?UTF-8?q?ner=20card=20cache=20to=20share=20legality=20status=20between?= =?UTF-8?q?=20cards=20with=20the=20same=20name=20-=20Process=20cards=20fro?= =?UTF-8?q?m=20newest=20to=20oldest=20sets=20during=20compilation=20(for?= =?UTF-8?q?=20cache)=20-=20Process=20Trainer=20cards=20sequentially,=20oth?= =?UTF-8?q?er=20cards=20in=20parallel=20for=20performance=20-=20Sort=20fin?= =?UTF-8?q?al=20output=20by=20release=20date=20(oldest=20first)=20to=20mai?= =?UTF-8?q?ntain=20original=20order=20-=20Add=20character=20name=20descrip?= =?UTF-8?q?tions=20to=20Trainer=20cards=20(e.g.,=20"Boss's=20Orders=20(Gio?= =?UTF-8?q?vanni)")=20=20This=20ensures=20that=20if=20a=20newer=20Trainer?= =?UTF-8?q?=20card=20with=20the=20same=20name=20is=20legal,=20older=20vers?= =?UTF-8?q?ions=20of=20that=20Trainer=20card=20will=20also=20be=20marked?= =?UTF-8?q?=20as=20legal,=20which=20aligns=20with=20how=20Trainer=20cards?= =?UTF-8?q?=20work=20in=20the=20Pok=C3=A9mon=20TCG=20where=20same-name=20T?= =?UTF-8?q?rainers=20share=20legality.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- data/Sword & Shield/Celebrations/23.ts | 13 +++- data/Sword & Shield/Celebrations/24.ts | 13 +++- data/Sword & Shield/Rebel Clash/154.ts | 13 +++- data/Sword & Shield/Rebel Clash/189.ts | 13 +++- data/Sword & Shield/Rebel Clash/200.ts | 13 +++- data/Sword & Shield/Shining Fates/58.ts | 15 +++- data/Sword & Shield/Shining Fates/60.ts | 13 +++- data/Sword & Shield/Sword & Shield/178.ts | 13 +++- data/Sword & Shield/Sword & Shield/201.ts | 13 +++- data/Sword & Shield/Sword & Shield/209.ts | 13 +++- server/compiler/endpoints/cards.ts | 95 ++++++++++++++++++++++- server/compiler/endpoints/series.ts | 12 ++- server/compiler/endpoints/sets.ts | 12 ++- server/compiler/utils/cardUtil.ts | 65 +++++++++++++--- server/compiler/utils/serieUtil.ts | 19 ++++- server/compiler/utils/setUtil.ts | 7 +- 16 files changed, 301 insertions(+), 41 deletions(-) diff --git a/data/Sword & Shield/Celebrations/23.ts b/data/Sword & Shield/Celebrations/23.ts index b265e3b8d3..eb4afd2f0b 100644 --- a/data/Sword & Shield/Celebrations/23.ts +++ b/data/Sword & Shield/Celebrations/23.ts @@ -12,12 +12,21 @@ const card: Card = { }, name: { + en: "Professor's Research", + fr: "Recherches Professorales", + es: "Investigación de Profesores", + it: "Ricerca Accademica", + pt: "Pesquisa de Professores", + de: "Forschung des Professors" + }, + + description: { en: "Professor's Research (Professor Oak)", fr: "Recherches Professorales (Professeur Chen)", es: "Investigación de Profesores (Profesor Oak)", it: "Ricerca Accademica (Professor Oak)", - pt: "Pesquisa de Professores", - de: "Forschung des Professors (Prof. Eich)" + pt: "Pesquisa de Professores (Professor Oak)", + de: "Forschung des Professors (Professor Eich)" }, rarity: "Holo Rare", diff --git a/data/Sword & Shield/Celebrations/24.ts b/data/Sword & Shield/Celebrations/24.ts index e8c232324e..892769e283 100644 --- a/data/Sword & Shield/Celebrations/24.ts +++ b/data/Sword & Shield/Celebrations/24.ts @@ -12,12 +12,21 @@ const card: Card = { }, name: { + en: "Professor's Research", + fr: "Recherches Professorales", + es: "Investigación de Profesores", + it: "Ricerca Accademica", + pt: "Pesquisa de Professores", + de: "Forschung des Professors" + }, + + description: { en: "Professor's Research (Professor Oak)", fr: "Recherches Professorales (Professeur Chen)", es: "Investigación de Profesores (Profesor Oak)", it: "Ricerca Accademica (Professor Oak)", - pt: "Pesquisa de Professores", - de: "Forschung des Professors (Prof. Eich)" + pt: "Pesquisa de Professores (Professor Oak)", + de: "Forschung des Professors (Professor Eich)" }, rarity: "Ultra Rare", diff --git a/data/Sword & Shield/Rebel Clash/154.ts b/data/Sword & Shield/Rebel Clash/154.ts index 672b889566..4a780ee8fa 100644 --- a/data/Sword & Shield/Rebel Clash/154.ts +++ b/data/Sword & Shield/Rebel Clash/154.ts @@ -3,8 +3,8 @@ import Set from '../Rebel Clash' const card: Card = { name: { - en: "Boss's Orders (Giovanni)", - fr: "Ordres du Boss (Giovanni)", + en: "Boss's Orders", + fr: "Ordres du Boss", es: "Órdenes de Jefes", it: "Ordini del Capo", pt: "Ordem da Chefia", @@ -28,6 +28,15 @@ const card: Card = { trainerType: "Supporter", regulationMark: "D", + description: { + en: "Boss's Orders (Giovanni)", + fr: "Ordres du Boss (Giovanni)", + es: "Órdenes de Jefes (Giovanni)", + it: "Ordini del Capo (Giovanni)", + pt: "Ordem da Chefia (Giovanni)", + de: "Befehl vom Boss (Giovanni)" + }, + variants: { normal: false, reverse: true, diff --git a/data/Sword & Shield/Rebel Clash/189.ts b/data/Sword & Shield/Rebel Clash/189.ts index ecdc3fe906..253fd42e20 100644 --- a/data/Sword & Shield/Rebel Clash/189.ts +++ b/data/Sword & Shield/Rebel Clash/189.ts @@ -3,14 +3,23 @@ import Set from '../Rebel Clash' const card: Card = { name: { - en: "Boss's Orders (Giovanni)", - fr: "Ordres du Boss (Giovanni)", + en: "Boss's Orders", + fr: "Ordres du Boss", es: "Órdenes de Jefes", it: "Ordini del Capo", pt: "Ordem da Chefia", de: "Befehl vom Boss" }, + description: { + en: "Boss's Orders (Giovanni)", + fr: "Ordres du Boss (Giovanni)", + es: "Órdenes de Jefes (Giovanni)", + it: "Ordini del Capo (Giovanni)", + pt: "Ordem da Chefia (Giovanni)", + de: "Befehl vom Boss (Giovanni)" + }, + illustrator: "nagimiso", rarity: "Ultra Rare", category: "Trainer", diff --git a/data/Sword & Shield/Rebel Clash/200.ts b/data/Sword & Shield/Rebel Clash/200.ts index 3a37a87d89..c45b576931 100644 --- a/data/Sword & Shield/Rebel Clash/200.ts +++ b/data/Sword & Shield/Rebel Clash/200.ts @@ -5,8 +5,8 @@ const card: Card = { set: Set, name: { - en: "Boss's Orders (Giovanni)", - fr: "Ordres du Boss (Giovanni)", + en: "Boss's Orders", + fr: "Ordres du Boss", es: "Órdenes de Jefes", it: "Ordini del Capo", pt: "Ordem da Chefia", @@ -29,6 +29,15 @@ const card: Card = { trainerType: "Supporter", regulationMark: "D", + description: { + en: "Boss's Orders (Giovanni)", + fr: "Ordres du Boss (Giovanni)", + es: "Órdenes de Jefes (Giovanni)", + it: "Ordini del Capo (Giovanni)", + pt: "Ordem da Chefia (Giovanni)", + de: "Befehl vom Boss (Giovanni)" + }, + variants: { normal: false, reverse: false, diff --git a/data/Sword & Shield/Shining Fates/58.ts b/data/Sword & Shield/Shining Fates/58.ts index 3fc6fbc3aa..9c7cf9fd38 100644 --- a/data/Sword & Shield/Shining Fates/58.ts +++ b/data/Sword & Shield/Shining Fates/58.ts @@ -5,14 +5,25 @@ const card: Card = { set: Set, name: { - fr: "Ordres du Boss (Lysandre)", - en: "Boss's Orders (Lysandre)", + en: "Boss's Orders", + fr: "Ordres du Boss", es: "Órdenes de Jefes", it: "Ordini del Capo", pt: "Ordem da Chefia", de: "Befehl vom Boss" }, + description: { + en: "Boss's Orders (Lysandre)", + fr: "Ordres du Boss (Lysandre)", + es: "Órdenes de Jefes (Lysson)", + 'es-mx': "Órdenes de Jefes (Lysandre)", + it: "Ordini del Capo (Elisio)", + pt: "Ordem da Chefia (Lysandre)", + 'pt-br': "Ordem da Chefia (Lysandre)", + de: "Befehl vom Boss (Flordelis)" + }, + illustrator: "Ryuta Fuse", rarity: "Rare", category: "Trainer", diff --git a/data/Sword & Shield/Shining Fates/60.ts b/data/Sword & Shield/Shining Fates/60.ts index 60bba5755c..487430e2ab 100644 --- a/data/Sword & Shield/Shining Fates/60.ts +++ b/data/Sword & Shield/Shining Fates/60.ts @@ -5,8 +5,8 @@ const card: Card = { set: Set, name: { - fr: "Recherches Professorales (Professeure Keteleeria)", - en: "Professor's Research (Professor Juniper)", + fr: "Recherches Professorales", + en: "Professor's Research", es: "Investigación de Profesores", it: "Ricerca Accademica", pt: "Pesquisa de Professores", @@ -29,6 +29,15 @@ const card: Card = { trainerType: "Supporter", regulationMark: "D", + description: { + en: "Professor's Research (Professor Juniper)", + de: "Forschung des Professors (Professor Esche)", + es: "Investigación de Profesores (Profesora Encina)", + fr: "Recherches Professorales (Professeure Keteleeria)", + it: "Ricerca Accademica (Professoressa Aralia)", + pt: "Pesquisa de Professores (Professor Juniper)" + }, + variants: { normal: true, reverse: true, diff --git a/data/Sword & Shield/Sword & Shield/178.ts b/data/Sword & Shield/Sword & Shield/178.ts index c7e4ad1a2d..62c07602cd 100644 --- a/data/Sword & Shield/Sword & Shield/178.ts +++ b/data/Sword & Shield/Sword & Shield/178.ts @@ -3,8 +3,8 @@ import Set from '../Sword & Shield' const card: Card = { name: { - en: "Professor's Research (Professor Magnolia)", - fr: "Recherches Professorales (Professeure Magnolia)", + en: "Professor's Research", + fr: "Recherches Professorales", es: "Investigación de Profesores", it: "Ricerca Accademica", pt: "Pesquisa de Professores", @@ -28,6 +28,15 @@ const card: Card = { trainerType: "Supporter", regulationMark: "D", + description: { + en: "Professor's Research (Professor Magnolia)", + de: "Forschung des Professors (Professor Magnolica)", + es: "Investigación de Profesores (Profesora Magnolia)", + fr: "Recherches Professorales (Professeure Magnolia)", + it: "Ricerca Accademica (Professoressa Flora)", + pt: "Pesquisa de Professores (Professor Juniper)" + }, + variants: { normal: false, reverse: true, diff --git a/data/Sword & Shield/Sword & Shield/201.ts b/data/Sword & Shield/Sword & Shield/201.ts index c3799fc512..61116441df 100644 --- a/data/Sword & Shield/Sword & Shield/201.ts +++ b/data/Sword & Shield/Sword & Shield/201.ts @@ -3,8 +3,8 @@ import Set from '../Sword & Shield' const card: Card = { name: { - en: "Professor's Research (Professor Magnolia)", - fr: "Recherches Professorales (Professeure Magnolia)", + en: "Professor's Research", + fr: "Recherches Professorales", es: "Investigación de Profesores", it: "Ricerca Accademica", pt: "Pesquisa de Professores", @@ -28,6 +28,15 @@ const card: Card = { trainerType: "Supporter", regulationMark: "D", + description: { + en: "Professor's Research (Professor Magnolia)", + de: "Forschung des Professors (Professor Magnolica)", + es: "Investigación de Profesores (Profesora Magnolia)", + fr: "Recherches Professorales (Professeure Magnolia)", + it: "Ricerca Accademica (Professoressa Flora)", + pt: "Pesquisa de Professores (Professor Juniper)" + }, + variants: { normal: false, reverse: false, diff --git a/data/Sword & Shield/Sword & Shield/209.ts b/data/Sword & Shield/Sword & Shield/209.ts index 8f687e17e0..afc454d5c5 100644 --- a/data/Sword & Shield/Sword & Shield/209.ts +++ b/data/Sword & Shield/Sword & Shield/209.ts @@ -3,8 +3,8 @@ import Set from '../Sword & Shield' const card: Card = { name: { - en: "Professor's Research (Professor Magnolia)", - fr: "Recherches Professorales (Professeure Magnolia)", + en: "Professor's Research", + fr: "Recherches Professorales", es: "Investigación de Profesores", it: "Ricerca Accademica", pt: "Pesquisa de Professores", @@ -28,6 +28,15 @@ const card: Card = { trainerType: "Supporter", regulationMark: "D", + description: { + en: "Professor's Research (Professor Magnolia)", + de: "Forschung des Professors (Professor Magnolica)", + es: "Investigación de Profesores (Profesora Magnolia)", + fr: "Recherches Professorales (Professeure Magnolia)", + it: "Ricerca Accademica (Professoressa Flora)", + pt: "Pesquisa de Professores (Professor Juniper)" + }, + variants: { normal: false, reverse: false, diff --git a/server/compiler/endpoints/cards.ts b/server/compiler/endpoints/cards.ts index 6a925c4f76..9e59b31b86 100644 --- a/server/compiler/endpoints/cards.ts +++ b/server/compiler/endpoints/cards.ts @@ -2,12 +2,99 @@ import { SupportedLanguages } from '../../../interfaces' import { FileFunction } from '../compilerInterfaces' import { cardToCardSingle, getCards } from '../utils/cardUtil' +// Cache to track Trainer card names and their legality status +interface TrainerLegalityCache { + [cardName: string]: { + standard: boolean + expanded: boolean + } +} + const fn: FileFunction = async (lang: SupportedLanguages) => { const common = await getCards(lang) - return await Promise.all(common.map((card) => cardToCardSingle(card[0], card[1], lang).catch((e) => { - console.error('error compiling card', `${card[1].set.id}-${card[0]}`, e) - throw e - }))) + const trainerCache: TrainerLegalityCache = {} + + // Separate cards into Trainers and non-Trainers + const trainerCards: Array<[string, typeof common[0][1]]> = [] + const otherCards: Array<[string, typeof common[0][1]]> = [] + + for (const [localId, card] of common) { + if (card.category === 'Trainer') { + trainerCards.push([localId, card]) + } else { + otherCards.push([localId, card]) + } + } + + console.log(`[DEBUG] Total cards: ${common.length}, Trainers: ${trainerCards.length}, Others: ${otherCards.length}`) + if (trainerCards.length > 0) { + console.log(`[DEBUG] First trainer card: ${trainerCards[0][1].name.en} (${trainerCards[0][1].set.id})`) + } + + // Process Trainer cards sequentially to build the cache + const trainerResults = [] + for (const [localId, card] of trainerCards) { + try { + const compiledCard = await cardToCardSingle(localId, card, lang, trainerCache) + trainerResults.push(compiledCard) + + // Update cache for Trainer cards using English name as key + const cardNameEn = card.name.en + if (cardNameEn) { + trainerCache[cardNameEn] = { + standard: compiledCard.legal.standard, + expanded: compiledCard.legal.expanded + } + if (trainerResults.length < 5) { + console.log(`[DEBUG] Cached trainer: ${cardNameEn} - standard: ${compiledCard.legal.standard}, expanded: ${compiledCard.legal.expanded}`) + } + } + } catch (e) { + console.error('error compiling card', `${card.set.id}-${localId}`, e) + throw e + } + } + + // Process other cards in parallel (they don't need the cache) + const otherResults = await Promise.all( + otherCards.map(([localId, card]) => + cardToCardSingle(localId, card, lang).catch((e) => { + console.error('error compiling card', `${card.set.id}-${localId}`, e) + throw e + }) + ) + ) + + const allResults = new Map() + + // Add trainer results + for (const result of trainerResults) { + allResults.set(result.id, result) + } + + // Add other results + for (const result of otherResults) { + allResults.set(result.id, result) + } + + const results = common.map(([localId, card]) => { + const cardId = `${card.set.id}-${localId}` + const result = allResults.get(cardId) + if (!result) { + throw new Error(`Missing compiled card: ${cardId}`) + } + return result + }) + + return results.sort((a, b) => { + const dateA = typeof a.set.releaseDate === 'object' + ? Object.values(a.set.releaseDate)[0] + : a.set.releaseDate + const dateB = typeof b.set.releaseDate === 'object' + ? Object.values(b.set.releaseDate)[0] + : b.set.releaseDate + return dateA > dateB ? 1 : -1 + }) } export default fn diff --git a/server/compiler/endpoints/series.ts b/server/compiler/endpoints/series.ts index 7c65c0f916..482086e0d8 100644 --- a/server/compiler/endpoints/series.ts +++ b/server/compiler/endpoints/series.ts @@ -4,7 +4,17 @@ import { getSeries, serieToSerieSingle } from '../utils/serieUtil' const fn: FileFunction = async (lang: SupportedLanguages) => { const common = await getSeries(lang) - return await Promise.all(common.map((val) => serieToSerieSingle(val, lang))) + const results = await Promise.all(common.map((val) => serieToSerieSingle(val, lang))) + + return results.sort((a, b) => { + const dateA = typeof a.releaseDate === 'object' + ? Object.values(a.releaseDate)[0] + : a.releaseDate + const dateB = typeof b.releaseDate === 'object' + ? Object.values(b.releaseDate)[0] + : b.releaseDate + return dateA > dateB ? 1 : -1 + }) } export default fn diff --git a/server/compiler/endpoints/sets.ts b/server/compiler/endpoints/sets.ts index 21ba3ed97d..d7430d0de1 100644 --- a/server/compiler/endpoints/sets.ts +++ b/server/compiler/endpoints/sets.ts @@ -5,7 +5,17 @@ import { FileFunction } from '../compilerInterfaces' const fn: FileFunction = async (lang: SupportedLanguages) => { const common = await getSets(undefined, lang) - return await Promise.all(common.map((set) => setToSetSingle(set, lang))) + const results = await Promise.all(common.map((set) => setToSetSingle(set, lang))) + + return results.sort((a, b) => { + const dateA = typeof a.releaseDate === 'object' + ? Object.values(a.releaseDate)[0] + : a.releaseDate + const dateB = typeof b.releaseDate === 'object' + ? Object.values(b.releaseDate)[0] + : b.releaseDate + return dateA > dateB ? 1 : -1 + }) } export default fn diff --git a/server/compiler/utils/cardUtil.ts b/server/compiler/utils/cardUtil.ts index 50a8e942ba..9e48f112f7 100644 --- a/server/compiler/utils/cardUtil.ts +++ b/server/compiler/utils/cardUtil.ts @@ -73,7 +73,12 @@ function variantsToVariantsDetailed(variants: CardSingle['variants'],lang: Suppo } // eslint-disable-next-line max-lines-per-function -export async function cardToCardSingle(localId: string, card: Card, lang: SupportedLanguages): Promise { +export async function cardToCardSingle( + localId: string, + card: Card, + lang: SupportedLanguages, + trainerCache?: { [cardName: string]: { standard: boolean; expanded: boolean } } +): Promise { const image = await getCardPictures(localId, card, lang) if (!card.name[lang]) { @@ -157,10 +162,39 @@ export async function cardToCardSingle(localId: string, card: Card, lang: Suppor energyType: translate('energyType', card.energyType, lang) as any, regulationMark: card.regulationMark, - legal: { - standard: cardIsLegal('standard', card, localId), - expanded: cardIsLegal('expanded', card, localId) - }, + legal: (() => { + // For Trainer cards, check if there's already a compiled card with the same name that is legal + // Use English name as key to optimize cache size + if (card.category === 'Trainer' && trainerCache) { + const cardNameEn = card.name.en + if (cardNameEn && trainerCache[cardNameEn]) { + // If a card with the same name is already legal, mark this card as legal too + const cachedLegal = trainerCache[cardNameEn] + const normalStandard = cardIsLegal('standard', card, localId) + const normalExpanded = cardIsLegal('expanded', card, localId) + const finalStandard = cachedLegal.standard ? true : normalStandard + const finalExpanded = cachedLegal.expanded ? true : normalExpanded + + // Debug: log when cache is used and changes the result + if (cachedLegal.standard && !normalStandard) { + console.log(`[DEBUG] Trainer cache override: ${cardNameEn} (${card.set.id}-${localId}) - standard: ${normalStandard} -> ${finalStandard}`) + } + if (cachedLegal.expanded && !normalExpanded) { + console.log(`[DEBUG] Trainer cache override: ${cardNameEn} (${card.set.id}-${localId}) - expanded: ${normalExpanded} -> ${finalExpanded}`) + } + + return { + standard: finalStandard, + expanded: finalExpanded + } + } + } + // Default behavior for non-Trainer cards or if no cache match + return { + standard: cardIsLegal('standard', card, localId), + expanded: cardIsLegal('expanded', card, localId) + } + })(), boosters: card.boosters ? objectMap(objectPick(card.set.boosters, ...card.boosters), (booster, id) => ({ id: `boo_${card.set.id}-${id}`, name: resolveText(booster.name, lang), @@ -226,14 +260,25 @@ export async function getCards(lang: SupportedLanguages, set?: Set): Promise { - const ra = parseInt(a, 10) - const rb = parseInt(b, 10) + return list.sort(([idA, cardA], [idB, cardB]) => { + const dateA = typeof cardA.set.releaseDate === 'object' + ? Object.values(cardA.set.releaseDate)[0] + : cardA.set.releaseDate + const dateB = typeof cardB.set.releaseDate === 'object' + ? Object.values(cardB.set.releaseDate)[0] + : cardB.set.releaseDate + + if (dateA !== dateB) { + return dateA > dateB ? -1 : 1 + } + + // Then sort by id when possible + const ra = parseInt(idA, 10) + const rb = parseInt(idB, 10) if (!isNaN(ra) && !isNaN(rb)) { return ra - rb } - return a >= b ? 1 : -1 + return idA >= idB ? 1 : -1 }) } diff --git a/server/compiler/utils/serieUtil.ts b/server/compiler/utils/serieUtil.ts index 42c0c67665..c2d8c9dea4 100644 --- a/server/compiler/utils/serieUtil.ts +++ b/server/compiler/utils/serieUtil.ts @@ -42,13 +42,22 @@ export async function getSeries(lang: SupportedLanguages): Promise> .reduce((p, c) => p ? p.releaseDate < c.releaseDate ? p : c : c, undefined) as Set ] as [Serie, Set])) - return tmp.sort((a, b) => (a[1] ? a[1].releaseDate : '0') > (b[1] ? b[1].releaseDate : '0') ? 1 : -1).map((it) => it[0]) + return tmp.sort((a, b) => { + if (!a[1] || !b[1]) return 0 + const dateA = typeof a[1].releaseDate === 'object' ? Object.values(a[1].releaseDate)[0] : a[1].releaseDate + const dateB = typeof b[1].releaseDate === 'object' ? Object.values(b[1].releaseDate)[0] : b[1].releaseDate + return dateA > dateB ? -1 : 1 + }).map((it) => it[0]) } export async function serieToSerieSimple(serie: Serie, lang: SupportedLanguages): Promise { const setsTmp = await getSets(getSerieIdenti(serie,lang), lang) const sets = await Promise.all(setsTmp - .sort((a, b) => a.releaseDate > b.releaseDate ? 1 : -1) + .sort((a, b) => { + const dateA = typeof a.releaseDate === 'object' ? Object.values(a.releaseDate)[0] : a.releaseDate + const dateB = typeof b.releaseDate === 'object' ? Object.values(b.releaseDate)[0] : b.releaseDate + return dateA > dateB ? -1 : 1 + }) .map((el) => setToSetSimple(el, lang))) const logo = sets.find((set) => set.logo)?.logo return { @@ -60,7 +69,11 @@ export async function serieToSerieSimple(serie: Serie, lang: SupportedLanguages) export async function serieToSerieSingle(serie: Serie, lang: SupportedLanguages): Promise { const setsTmp = await getSets(getSerieIdenti(serie,lang), lang) - const sortedSetsTmp = setsTmp.sort((a, b) => a.releaseDate > b.releaseDate ? 1 : -1) + const sortedSetsTmp = setsTmp.sort((a, b) => { + const dateA = typeof a.releaseDate === 'object' ? Object.values(a.releaseDate)[0] : a.releaseDate + const dateB = typeof b.releaseDate === 'object' ? Object.values(b.releaseDate)[0] : b.releaseDate + return dateA > dateB ? -1 : 1 + }) const sets = await Promise.all(sortedSetsTmp.map((el) => setToSetSimple(el, lang))) const logo = ( // find the set named after the serie diff --git a/server/compiler/utils/setUtil.ts b/server/compiler/utils/setUtil.ts index 81856c8dc8..1bfa4d60b4 100644 --- a/server/compiler/utils/setUtil.ts +++ b/server/compiler/utils/setUtil.ts @@ -46,8 +46,11 @@ export async function getSets(serie = '*', lang: SupportedLanguages): Promise getSet(set, serie, lang)))) // Filter sets .filter((set) => isSetAvailable(set, lang)) - // Sort sets by release date - .sort((a, b) => a.releaseDate > b.releaseDate ? 1 : -1) + .sort((a, b) => { + const dateA = typeof a.releaseDate === 'object' ? Object.values(a.releaseDate)[0] : a.releaseDate + const dateB = typeof b.releaseDate === 'object' ? Object.values(b.releaseDate)[0] : b.releaseDate + return dateA > dateB ? -1 : 1 + }) return sets } From 401a3487c7289f2ec729bdb2552bee996043d023 Mon Sep 17 00:00:00 2001 From: Kevin Vacquier Date: Fri, 5 Dec 2025 00:35:31 +0100 Subject: [PATCH 2/2] Cleaning up for card leality post compilation / pre-writting --- server/compiler/endpoints/cards.ts | 95 +-------------------- server/compiler/endpoints/series.ts | 12 +-- server/compiler/endpoints/sets.ts | 12 +-- server/compiler/index.ts | 13 ++- server/compiler/utils/cardUtil.ts | 125 ++++++++++++++++------------ server/compiler/utils/serieUtil.ts | 19 +---- server/compiler/utils/setUtil.ts | 7 +- 7 files changed, 92 insertions(+), 191 deletions(-) diff --git a/server/compiler/endpoints/cards.ts b/server/compiler/endpoints/cards.ts index 9e59b31b86..6a925c4f76 100644 --- a/server/compiler/endpoints/cards.ts +++ b/server/compiler/endpoints/cards.ts @@ -2,99 +2,12 @@ import { SupportedLanguages } from '../../../interfaces' import { FileFunction } from '../compilerInterfaces' import { cardToCardSingle, getCards } from '../utils/cardUtil' -// Cache to track Trainer card names and their legality status -interface TrainerLegalityCache { - [cardName: string]: { - standard: boolean - expanded: boolean - } -} - const fn: FileFunction = async (lang: SupportedLanguages) => { const common = await getCards(lang) - const trainerCache: TrainerLegalityCache = {} - - // Separate cards into Trainers and non-Trainers - const trainerCards: Array<[string, typeof common[0][1]]> = [] - const otherCards: Array<[string, typeof common[0][1]]> = [] - - for (const [localId, card] of common) { - if (card.category === 'Trainer') { - trainerCards.push([localId, card]) - } else { - otherCards.push([localId, card]) - } - } - - console.log(`[DEBUG] Total cards: ${common.length}, Trainers: ${trainerCards.length}, Others: ${otherCards.length}`) - if (trainerCards.length > 0) { - console.log(`[DEBUG] First trainer card: ${trainerCards[0][1].name.en} (${trainerCards[0][1].set.id})`) - } - - // Process Trainer cards sequentially to build the cache - const trainerResults = [] - for (const [localId, card] of trainerCards) { - try { - const compiledCard = await cardToCardSingle(localId, card, lang, trainerCache) - trainerResults.push(compiledCard) - - // Update cache for Trainer cards using English name as key - const cardNameEn = card.name.en - if (cardNameEn) { - trainerCache[cardNameEn] = { - standard: compiledCard.legal.standard, - expanded: compiledCard.legal.expanded - } - if (trainerResults.length < 5) { - console.log(`[DEBUG] Cached trainer: ${cardNameEn} - standard: ${compiledCard.legal.standard}, expanded: ${compiledCard.legal.expanded}`) - } - } - } catch (e) { - console.error('error compiling card', `${card.set.id}-${localId}`, e) - throw e - } - } - - // Process other cards in parallel (they don't need the cache) - const otherResults = await Promise.all( - otherCards.map(([localId, card]) => - cardToCardSingle(localId, card, lang).catch((e) => { - console.error('error compiling card', `${card.set.id}-${localId}`, e) - throw e - }) - ) - ) - - const allResults = new Map() - - // Add trainer results - for (const result of trainerResults) { - allResults.set(result.id, result) - } - - // Add other results - for (const result of otherResults) { - allResults.set(result.id, result) - } - - const results = common.map(([localId, card]) => { - const cardId = `${card.set.id}-${localId}` - const result = allResults.get(cardId) - if (!result) { - throw new Error(`Missing compiled card: ${cardId}`) - } - return result - }) - - return results.sort((a, b) => { - const dateA = typeof a.set.releaseDate === 'object' - ? Object.values(a.set.releaseDate)[0] - : a.set.releaseDate - const dateB = typeof b.set.releaseDate === 'object' - ? Object.values(b.set.releaseDate)[0] - : b.set.releaseDate - return dateA > dateB ? 1 : -1 - }) + return await Promise.all(common.map((card) => cardToCardSingle(card[0], card[1], lang).catch((e) => { + console.error('error compiling card', `${card[1].set.id}-${card[0]}`, e) + throw e + }))) } export default fn diff --git a/server/compiler/endpoints/series.ts b/server/compiler/endpoints/series.ts index 482086e0d8..7c65c0f916 100644 --- a/server/compiler/endpoints/series.ts +++ b/server/compiler/endpoints/series.ts @@ -4,17 +4,7 @@ import { getSeries, serieToSerieSingle } from '../utils/serieUtil' const fn: FileFunction = async (lang: SupportedLanguages) => { const common = await getSeries(lang) - const results = await Promise.all(common.map((val) => serieToSerieSingle(val, lang))) - - return results.sort((a, b) => { - const dateA = typeof a.releaseDate === 'object' - ? Object.values(a.releaseDate)[0] - : a.releaseDate - const dateB = typeof b.releaseDate === 'object' - ? Object.values(b.releaseDate)[0] - : b.releaseDate - return dateA > dateB ? 1 : -1 - }) + return await Promise.all(common.map((val) => serieToSerieSingle(val, lang))) } export default fn diff --git a/server/compiler/endpoints/sets.ts b/server/compiler/endpoints/sets.ts index d7430d0de1..21ba3ed97d 100644 --- a/server/compiler/endpoints/sets.ts +++ b/server/compiler/endpoints/sets.ts @@ -5,17 +5,7 @@ import { FileFunction } from '../compilerInterfaces' const fn: FileFunction = async (lang: SupportedLanguages) => { const common = await getSets(undefined, lang) - const results = await Promise.all(common.map((set) => setToSetSingle(set, lang))) - - return results.sort((a, b) => { - const dateA = typeof a.releaseDate === 'object' - ? Object.values(a.releaseDate)[0] - : a.releaseDate - const dateB = typeof b.releaseDate === 'object' - ? Object.values(b.releaseDate)[0] - : b.releaseDate - return dateA > dateB ? 1 : -1 - }) + return await Promise.all(common.map((set) => setToSetSingle(set, lang))) } export default fn diff --git a/server/compiler/index.ts b/server/compiler/index.ts index 582d190542..b7e6bf8508 100644 --- a/server/compiler/index.ts +++ b/server/compiler/index.ts @@ -1,8 +1,10 @@ /* eslint-disable max-statements */ -import { existsSync, promises as fs } from 'fs' +import { promises as fs } from 'fs' import { SupportedLanguages } from '../../interfaces' import { FileFunction } from './compilerInterfaces' import { fetchRemoteFile, loadLastEdits } from './utils/util' +import { enhanceTrainerLegality, getCards } from './utils/cardUtil' +import { Card as CardSingle } from '../../meta/definitions/api' const LANGS: Array = [ 'en', 'fr', 'es', 'es-mx', 'it', 'pt', 'pt-br', 'pt-pt', 'de', 'nl', 'pl', 'ru', @@ -54,7 +56,14 @@ const DIST_FOLDER = './generated' // Run the function console.log(' ', 'Compiling', lang, file) - const item = await fn(lang) + let item = await fn(lang) + + // Post-process Trainer legality after compilation but before writing JSON + if (file === 'cards.ts' && Array.isArray(item)) { + console.log(' ', 'Post-processing Trainer legality', lang) + const originalCards = await getCards(lang) + item = enhanceTrainerLegality(item as Array, originalCards) + } // Write to file await fs.writeFile(`${folder}/${file.replace('.ts', '')}.json`, JSON.stringify( diff --git a/server/compiler/utils/cardUtil.ts b/server/compiler/utils/cardUtil.ts index 9e48f112f7..518f1b13e9 100644 --- a/server/compiler/utils/cardUtil.ts +++ b/server/compiler/utils/cardUtil.ts @@ -8,6 +8,66 @@ import { DB_PATH, cardIsLegal, fetchRemoteFile, getDataFolder, getLastEdit, reso import { objectMap, objectPick } from '@dzeio/object-util' import { variant_detailed } from "../../public/v2/api"; +/** + * Post-processes compiled cards to enhance Trainer legality based on reprints. + * If any reprint of a Trainer card is legal, all cards with the same English name are marked as legal. + */ +export function enhanceTrainerLegality( + compiledCards: Array, + originalCards: Array<[string, Card]> +): Array { + const originalCardMap = new Map() + for (const [localId, card] of originalCards) { + const cardId = `${card.set.id}-${localId}` + originalCardMap.set(cardId, card) + } + + const trainerCardsByName = new Map>() + + for (const compiledCard of compiledCards) { + const originalCard = originalCardMap.get(compiledCard.id) + if (!originalCard || originalCard.category !== 'Trainer') { + continue + } + + const cardNameEn = originalCard.name.en + if (!cardNameEn) { + continue + } + + if (!trainerCardsByName.has(cardNameEn)) { + trainerCardsByName.set(cardNameEn, []) + } + + trainerCardsByName.get(cardNameEn)!.push({ + compiled: compiledCard, + original: originalCard, + localId: compiledCard.localId + }) + } + + for (const [cardNameEn, cardsWithName] of trainerCardsByName) { + let hasLegalStandard = false + let hasLegalExpanded = false + + for (const { compiled } of cardsWithName) { + if (compiled.legal.standard) { + hasLegalStandard = true + } + if (compiled.legal.expanded) { + hasLegalExpanded = true + } + } + + for (const { compiled } of cardsWithName) { + compiled.legal.standard = hasLegalStandard || compiled.legal.standard + compiled.legal.expanded = hasLegalExpanded || compiled.legal.expanded + } + } + + return compiledCards +} + export async function getCardPictures(cardId: string, card: Card, lang: SupportedLanguages): Promise { try { const file = await fetchRemoteFile('https://assets.tcgdex.net/datas.json') @@ -73,12 +133,7 @@ function variantsToVariantsDetailed(variants: CardSingle['variants'],lang: Suppo } // eslint-disable-next-line max-lines-per-function -export async function cardToCardSingle( - localId: string, - card: Card, - lang: SupportedLanguages, - trainerCache?: { [cardName: string]: { standard: boolean; expanded: boolean } } -): Promise { +export async function cardToCardSingle(localId: string, card: Card, lang: SupportedLanguages): Promise { const image = await getCardPictures(localId, card, lang) if (!card.name[lang]) { @@ -162,39 +217,10 @@ export async function cardToCardSingle( energyType: translate('energyType', card.energyType, lang) as any, regulationMark: card.regulationMark, - legal: (() => { - // For Trainer cards, check if there's already a compiled card with the same name that is legal - // Use English name as key to optimize cache size - if (card.category === 'Trainer' && trainerCache) { - const cardNameEn = card.name.en - if (cardNameEn && trainerCache[cardNameEn]) { - // If a card with the same name is already legal, mark this card as legal too - const cachedLegal = trainerCache[cardNameEn] - const normalStandard = cardIsLegal('standard', card, localId) - const normalExpanded = cardIsLegal('expanded', card, localId) - const finalStandard = cachedLegal.standard ? true : normalStandard - const finalExpanded = cachedLegal.expanded ? true : normalExpanded - - // Debug: log when cache is used and changes the result - if (cachedLegal.standard && !normalStandard) { - console.log(`[DEBUG] Trainer cache override: ${cardNameEn} (${card.set.id}-${localId}) - standard: ${normalStandard} -> ${finalStandard}`) - } - if (cachedLegal.expanded && !normalExpanded) { - console.log(`[DEBUG] Trainer cache override: ${cardNameEn} (${card.set.id}-${localId}) - expanded: ${normalExpanded} -> ${finalExpanded}`) - } - - return { - standard: finalStandard, - expanded: finalExpanded - } - } - } - // Default behavior for non-Trainer cards or if no cache match - return { - standard: cardIsLegal('standard', card, localId), - expanded: cardIsLegal('expanded', card, localId) - } - })(), + legal: { + standard: cardIsLegal('standard', card, localId), + expanded: cardIsLegal('expanded', card, localId) + }, boosters: card.boosters ? objectMap(objectPick(card.set.boosters, ...card.boosters), (booster, id) => ({ id: `boo_${card.set.id}-${id}`, name: resolveText(booster.name, lang), @@ -260,25 +286,14 @@ export async function getCards(lang: SupportedLanguages, set?: Set): Promise { - const dateA = typeof cardA.set.releaseDate === 'object' - ? Object.values(cardA.set.releaseDate)[0] - : cardA.set.releaseDate - const dateB = typeof cardB.set.releaseDate === 'object' - ? Object.values(cardB.set.releaseDate)[0] - : cardB.set.releaseDate - - if (dateA !== dateB) { - return dateA > dateB ? -1 : 1 - } - - // Then sort by id when possible - const ra = parseInt(idA, 10) - const rb = parseInt(idB, 10) + // Sort by id when possible + return list.sort(([a], [b]) => { + const ra = parseInt(a, 10) + const rb = parseInt(b, 10) if (!isNaN(ra) && !isNaN(rb)) { return ra - rb } - return idA >= idB ? 1 : -1 + return a >= b ? 1 : -1 }) } diff --git a/server/compiler/utils/serieUtil.ts b/server/compiler/utils/serieUtil.ts index c2d8c9dea4..42c0c67665 100644 --- a/server/compiler/utils/serieUtil.ts +++ b/server/compiler/utils/serieUtil.ts @@ -42,22 +42,13 @@ export async function getSeries(lang: SupportedLanguages): Promise> .reduce((p, c) => p ? p.releaseDate < c.releaseDate ? p : c : c, undefined) as Set ] as [Serie, Set])) - return tmp.sort((a, b) => { - if (!a[1] || !b[1]) return 0 - const dateA = typeof a[1].releaseDate === 'object' ? Object.values(a[1].releaseDate)[0] : a[1].releaseDate - const dateB = typeof b[1].releaseDate === 'object' ? Object.values(b[1].releaseDate)[0] : b[1].releaseDate - return dateA > dateB ? -1 : 1 - }).map((it) => it[0]) + return tmp.sort((a, b) => (a[1] ? a[1].releaseDate : '0') > (b[1] ? b[1].releaseDate : '0') ? 1 : -1).map((it) => it[0]) } export async function serieToSerieSimple(serie: Serie, lang: SupportedLanguages): Promise { const setsTmp = await getSets(getSerieIdenti(serie,lang), lang) const sets = await Promise.all(setsTmp - .sort((a, b) => { - const dateA = typeof a.releaseDate === 'object' ? Object.values(a.releaseDate)[0] : a.releaseDate - const dateB = typeof b.releaseDate === 'object' ? Object.values(b.releaseDate)[0] : b.releaseDate - return dateA > dateB ? -1 : 1 - }) + .sort((a, b) => a.releaseDate > b.releaseDate ? 1 : -1) .map((el) => setToSetSimple(el, lang))) const logo = sets.find((set) => set.logo)?.logo return { @@ -69,11 +60,7 @@ export async function serieToSerieSimple(serie: Serie, lang: SupportedLanguages) export async function serieToSerieSingle(serie: Serie, lang: SupportedLanguages): Promise { const setsTmp = await getSets(getSerieIdenti(serie,lang), lang) - const sortedSetsTmp = setsTmp.sort((a, b) => { - const dateA = typeof a.releaseDate === 'object' ? Object.values(a.releaseDate)[0] : a.releaseDate - const dateB = typeof b.releaseDate === 'object' ? Object.values(b.releaseDate)[0] : b.releaseDate - return dateA > dateB ? -1 : 1 - }) + const sortedSetsTmp = setsTmp.sort((a, b) => a.releaseDate > b.releaseDate ? 1 : -1) const sets = await Promise.all(sortedSetsTmp.map((el) => setToSetSimple(el, lang))) const logo = ( // find the set named after the serie diff --git a/server/compiler/utils/setUtil.ts b/server/compiler/utils/setUtil.ts index 1bfa4d60b4..81856c8dc8 100644 --- a/server/compiler/utils/setUtil.ts +++ b/server/compiler/utils/setUtil.ts @@ -46,11 +46,8 @@ export async function getSets(serie = '*', lang: SupportedLanguages): Promise getSet(set, serie, lang)))) // Filter sets .filter((set) => isSetAvailable(set, lang)) - .sort((a, b) => { - const dateA = typeof a.releaseDate === 'object' ? Object.values(a.releaseDate)[0] : a.releaseDate - const dateB = typeof b.releaseDate === 'object' ? Object.values(b.releaseDate)[0] : b.releaseDate - return dateA > dateB ? -1 : 1 - }) + // Sort sets by release date + .sort((a, b) => a.releaseDate > b.releaseDate ? 1 : -1) return sets }