feat: build compact BO2 grind companion with cumulative ranks
Some checks failed
Test and deploy staging / staging (push) Failing after 2m50s

This commit is contained in:
2026-10-08 21:06:19 +00:00
parent c2ba8febb4
commit c2bc9050c2
39 changed files with 1499 additions and 447 deletions

View File

@@ -12,6 +12,7 @@ import {
} from '$lib/server/db/schema';
import { loadPokemonProgress } from '$lib/server/tracker-progress';
import { challengeProgress, combinedProgress, sumProgress } from '$lib/game-tracker/progress';
import { gameRanksProgress, hasReachedMaxLevel } from '$lib/game-tracker/rank-progress';
import { summarizeGame } from '$lib/game-tracker/mastery';
import { chronologicalPokemon, pokemonRelease } from '$lib/game-tracker/game-art';
import type { PageServerLoad } from './$types';
@@ -53,13 +54,21 @@ async function callOfDutySummary(userId: string | undefined) {
: Promise.resolve([]),
userId
? db
.select({ itemId: trackerWeaponRanks.itemId, prestige: trackerWeaponRanks.prestige })
.select({
itemId: trackerWeaponRanks.itemId,
prestige: trackerWeaponRanks.prestige,
level: trackerWeaponRanks.level
})
.from(trackerWeaponRanks)
.where(eq(trackerWeaponRanks.userId, userId))
: Promise.resolve([]),
userId
? db
.select({ gameId: trackerRanks.gameId })
.select({
gameId: trackerRanks.gameId,
level: trackerRanks.level,
prestige: trackerRanks.prestige
})
.from(trackerRanks)
.where(eq(trackerRanks.userId, userId))
: Promise.resolve([]),
@@ -86,6 +95,11 @@ async function callOfDutySummary(userId: string | undefined) {
const byGame = Map.groupBy(items, (item) => item.gameId);
return gamesList.map((game) => {
const list = byGame.get(game.id) ?? [];
const weapons = list.filter((i) => i.kind === 'weapon');
const rankMap = new Map(ranks.map((r) => [r.itemId, r]));
for (const weapon of weapons)
if (hasReachedMaxLevel(game.rules, weapon, rankMap.get(weapon.id)))
done.add(`${weapon.id}:maxLevel`);
const summary = summarizeGame(
game.rules,
list.filter((item) => item.kind === 'weapon'),
@@ -98,13 +112,12 @@ async function callOfDutySummary(userId: string | undefined) {
challenges.filter((c) => c.gameId === game.id),
challengeRows
);
const ranksProgress = {
value:
ranks.filter((rank) =>
list.some((item) => item.kind === 'weapon' && item.id === rank.itemId)
).length + Number(playerRanks.some((rank) => rank.gameId === game.id)),
total: list.filter((item) => item.kind === 'weapon').length + 1
};
const ranksProgress = gameRanksProgress(
game.rules,
weapons,
rankMap,
playerRanks.find((r) => r.gameId === game.id)
);
return {
overall: combinedProgress([
weaponProgress,

View File

@@ -145,8 +145,10 @@ export const actions = {
.where(
and(eq(trackerWeaponRanks.userId, locals.user.id), eq(trackerWeaponRanks.itemId, itemId))
);
if (!current || current.level < maxLevel || current.prestige >= limits.maxPrestige)
if (!current || current.level < maxLevel)
return fail(400, { message: `Reach weapon level ${maxLevel} before increasing prestige.` });
if (current.prestige >= limits.maxPrestige)
return fail(400, { message: `This weapon is already at prestige ${limits.maxPrestige}.` });
}
// Arithmetic happens in Postgres, so simultaneous taps from different devices cannot lose a level.
const [rank] = await db
@@ -169,13 +171,18 @@ export const actions = {
...(delta > 0 ? { level: 1 } : {})
}),
updatedAt: new Date()
}
},
setWhere:
field === 'prestige' && delta > 0 && limits.canPrestigeOnlyAtMaxLevel
? sql`${trackerWeaponRanks.level} >= ${maxLevel} and ${trackerWeaponRanks.prestige} < ${limits.maxPrestige}`
: undefined
})
.returning({
itemId: trackerWeaponRanks.itemId,
level: trackerWeaponRanks.level,
prestige: trackerWeaponRanks.prestige
});
if (!rank) return fail(400, { message: 'Rank changed. Refresh before prestiging again.' });
return { rank };
},
saveRank: async ({ request, locals, params }) => {

View File

@@ -1,8 +1,10 @@
import weaponArt from '../../../../../data/cod-art.json';
import rankArt from '../../../../../data/cod-rank-art.json';
import images from '../../../../../data/game-images.json';
import maps from '../../../../../data/pokemon-map-art.json';
export function load() {
const sources = new Map<string, { source: string; credit: string; count: number }>();
for (const image of images) {
for (const image of [...images, ...weaponArt, ...rankArt]) {
const existing = sources.get(image.source);
if (existing) existing.count++;
else sources.set(image.source, { source: image.source, credit: image.credit, count: 1 });
@@ -10,6 +12,6 @@ export function load() {
return {
sources: [...sources.values()],
maps: Object.values(maps).map(({ name, source, credit }) => ({ name, source, credit })),
total: images.length
total: images.length + weaponArt.length + rankArt.length
};
}