import achievementArt from '../data/cod-achievement-art.json'; const achievementImages: Record = achievementArt; import artwork from '../data/cod-art.json'; import { readFile } from 'node:fs/promises'; import { drizzle } from 'drizzle-orm/postgres-js'; import { and, eq, inArray, notInArray, sql } from 'drizzle-orm'; import postgres from 'postgres'; import { trackerGames, trackerItems, trackerProgress } from '../src/lib/server/db/schema/game-tracker'; import type { MasteryRules } from '../src/lib/game-tracker/mastery'; type Catalog = { games: (Omit & { slug: string; name: string; shortName: string; releaseYear: number; classes: { name: string; weapons: string[]; postLaunch?: string[] }[]; achievements: { key: string; category: string; name: string; description: string | null; icon: string; }[]; })[]; }; // Keys are derived from weapon names, so renaming a weapon starts its progress over. const weaponKey = (name: string) => `weapon:${name .toLowerCase() .replace(/[^a-z0-9]+/g, '-') .replace(/^-|-$/g, '')}`; if (!process.env.DB_URL) throw new Error('DB_URL is required'); const catalog: Catalog = JSON.parse( await readFile(new URL('../data/call-of-duty.json', import.meta.url), 'utf8') ); const client = postgres(process.env.DB_URL, { max: 1 }); const db = drizzle({ client }); try { await db.transaction(async (tx) => { for (const [order, game] of catalog.games.entries()) { const rules: MasteryRules = { milestones: game.milestones, tiers: game.tiers, classes: game.classes.map((c) => c.name), steamApps: game.steamApps, rankCaps: game.rankCaps }; const values = { slug: game.slug, franchise: 'call-of-duty', name: game.name, shortName: game.shortName, releaseYear: game.releaseYear, sortOrder: order, rules }; const [saved] = await tx .insert(trackerGames) .values(values) .onConflictDoUpdate({ target: trackerGames.slug, set: values }) .returning(); const weapons = game.classes.flatMap((c) => [ ...c.weapons.map((name) => ({ name, postLaunch: false })), ...(c.postLaunch ?? []).map((name) => ({ name, postLaunch: true })) ].map((weapon) => ({ ...weapon, category: c.name })) ); const items = [ ...weapons.map((weapon, index) => ({ gameId: saved.id, key: weaponKey(weapon.name), kind: 'weapon' as const, category: weapon.category, name: weapon.name, description: null, icon: artwork.find((a) => a.game === game.slug && a.key === weaponKey(weapon.name))?.path ?? null, postLaunch: weapon.postLaunch, sortOrder: index })), ...game.achievements.map((achievement, index) => ({ gameId: saved.id, key: achievement.key, kind: 'achievement' as const, category: achievement.category, name: achievement.name, description: achievement.description, icon: achievementImages[achievement.icon] ?? achievement.icon, postLaunch: false, sortOrder: index })) ]; await tx .insert(trackerItems) .values(items) .onConflictDoUpdate({ target: [trackerItems.gameId, trackerItems.key], set: { kind: sql`excluded.kind`, category: sql`excluded.category`, name: sql`excluded.name`, description: sql`excluded.description`, icon: sql`excluded.icon`, postLaunch: sql`excluded.post_launch`, sortOrder: sql`excluded.sort_order` } }); // Items dropped from the catalog disappear unless someone has progress on them. const stale = await tx .select({ id: trackerItems.id, name: trackerItems.name }) .from(trackerItems) .where( and( eq(trackerItems.gameId, saved.id), notInArray( trackerItems.key, items.map((item) => item.key) ) ) ); if (stale.length) { const kept = await tx .selectDistinct({ id: trackerProgress.itemId }) .from(trackerProgress) .where( inArray( trackerProgress.itemId, stale.map((item) => item.id) ) ); const keep = new Set(kept.map((row) => row.id)); const remove = stale.filter((item) => !keep.has(item.id)).map((item) => item.id); if (remove.length) await tx.delete(trackerItems).where(inArray(trackerItems.id, remove)); for (const item of stale.filter((item) => keep.has(item.id))) console.warn( `${game.shortName}: kept "${item.name}", which has progress but left the catalog` ); } console.log( `${game.shortName}: ${weapons.length} weapons, ${game.achievements.length} achievements` ); } }); } finally { await client.end(); }