Files
personal-website/scripts/seed-cod-catalog.ts
Stan Runge a10585a4d2
All checks were successful
Test and deploy staging / staging (push) Successful in 18s
add game tracker with owned pokemon data and call of duty mastery
Fold catchemall into /projects/game-tracker (old URL redirects) and add
Call of Duty tracking for Black Ops through WWII.

- Pokémon catalog is now owned: locations, encounters and per-game
  evolution methods are built from the pinned PokéAPI snapshot and seeded.
- Pokémon you can evolve or breed in a game count as obtainable; mythicals
  without another route are marked event-only.
- Detail dialog with evolution chain and where to find it, location pages
  with admin notes and pictures, and admin corrections that survive reseeds.
- Call of Duty weapons, milestones and Steam achievements per game, with
  derived Diamond / Dark Matter / Solar / Black Sky / Chrome tiers.
- New migration (db:migrate:game-tracker), seeds (db:seed, db:seed:cod) and
  deploy preflight checks for them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 17:36:18 +00:00

149 lines
4.2 KiB
TypeScript

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<MasteryRules, 'classes'> & {
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
};
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: 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: 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();
}