add game tracker with owned pokemon data and call of duty mastery
All checks were successful
Test and deploy staging / staging (push) Successful in 18s
All checks were successful
Test and deploy staging / staging (push) Successful in 18s
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>
This commit is contained in:
148
scripts/seed-cod-catalog.ts
Normal file
148
scripts/seed-cod-catalog.ts
Normal file
@@ -0,0 +1,148 @@
|
||||
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();
|
||||
}
|
||||
Reference in New Issue
Block a user