feat: roomier BO2 companion with +/- rank stepping
All checks were successful
Test and deploy staging / staging (push) Successful in 2m29s

- Scale the COD page to the screen: larger gun art, text and progress
  ribbon, sized from one viewport unit so it fills a MacBook Pro 16"
  and big monitors and still fits 1280x720 without scrolling.
- Five columns on wide screens and four below, picked by CSS, so the
  server-rendered page already has the final layout.
- Replace the tabs/filters/overview rows with one toolbar of matching
  controls.
- Player and weapon ranks step with + and -: level 55 + 1 is the next
  prestige at level 1, up to max prestige. Presses show at once, batch
  while a save is running, and stepRank does the arithmetic in SQL.
- Clicking a rank still opens exact entry for big jumps.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-08 21:50:19 +00:00
parent 8eec207f2d
commit 0ee9ed0abb
10 changed files with 1128 additions and 588 deletions

View File

@@ -2,6 +2,7 @@ import { error, fail } from '@sveltejs/kit';
import achievementArt from '../../../../../../data/cod-achievement-art.json';
const achievementImages: Record<string, string> = achievementArt;
import { and, asc, eq, sql } from 'drizzle-orm';
import type { AnyPgColumn } from 'drizzle-orm/pg-core';
import { db } from '$lib/server/db';
import {
trackerGames,
@@ -14,7 +15,8 @@ import {
} from '$lib/server/db/schema';
import { boundedInteger } from '$lib/game-tracker/journal';
import { appliesTo } from '$lib/game-tracker/mastery';
import { playerRankLimits, weaponRankLimits } from '$lib/game-tracker/rank-limits';
import { playerRankLimits, weaponRankLimits, type RankLimits } from '$lib/game-tracker/rank-limits';
import { maxUncappedLevel, stepRank } from '$lib/game-tracker/rank-progress';
import type { Actions, PageServerLoad } from './$types';
async function findGame(slug: string) {
@@ -111,16 +113,45 @@ export const load: PageServerLoad = async ({ params, locals }) => {
};
};
// SQL twin of stepRank: computed from the stored row, so taps from several devices never lose a level.
function steppedRank(level: AnyPgColumn, prestige: AnyPgColumn, limits: RankLimits, delta: number) {
if (limits.maxLevel === null)
return { level: sql`greatest(1, least(${maxUncappedLevel}::int, ${level} + ${delta}::int))` };
const cap = limits.maxLevel;
const total = cap * (limits.maxPrestige + 1);
const next = sql`(greatest(1, least(${total}::int,
least(greatest(${prestige}, 0), ${limits.maxPrestige}::int) * ${cap}::int
+ greatest(1, least(${level}, ${cap}::int)) + ${delta}::int)) - 1)`;
return { level: sql`${next} % ${cap}::int + 1`, prestige: sql`${next} / ${cap}::int` };
}
export const actions = {
adjustWeaponRank: async ({ request, locals, params }) => {
if (!locals.user) return fail(401, { message: 'Sign in to save weapon progress.' });
// One + / − press (or a few batched presses) on the player rank or a weapon rank.
stepRank: async ({ request, locals, params }) => {
if (!locals.user) return fail(401, { message: 'Sign in to save your rank.' });
const game = await findGame(params.slug);
const form = await request.formData();
const itemId = boundedInteger(form.get('itemId'), 1, 2147483647);
const field = form.get('field');
const delta = form.get('delta') === '1' ? 1 : form.get('delta') === '-1' ? -1 : null;
if (!itemId || !['level', 'prestige'].includes(String(field)) || delta === null)
return fail(400, { message: 'Invalid weapon adjustment.' });
const rawDelta = String(form.get('delta') ?? '');
const delta = /^-?\d{1,3}$/.test(rawDelta) ? Number(rawDelta) : 0;
const itemId = form.get('itemId') ? boundedInteger(form.get('itemId'), 1, 2147483647) : null;
if (!delta || Math.abs(delta) > 100 || (form.get('itemId') && !itemId))
return fail(400, { message: 'Invalid rank change.' });
const updatedAt = new Date();
if (!itemId) {
const limits = playerRankLimits(game.rules);
const [rank] = await db
.insert(trackerRanks)
.values({ userId: locals.user.id, gameId: game.id, ...stepRank(limits, null, delta) })
.onConflictDoUpdate({
target: [trackerRanks.userId, trackerRanks.gameId],
set: {
...steppedRank(trackerRanks.level, trackerRanks.prestige, limits, delta),
updatedAt
}
})
.returning({ level: trackerRanks.level, prestige: trackerRanks.prestige });
return { rank: { itemId: null, ...rank } };
}
const [item] = await db
.select({ id: trackerItems.id, name: trackerItems.name, category: trackerItems.category })
.from(trackerItems)
@@ -137,52 +168,21 @@ export const actions = {
return fail(400, {
message: 'This weapon does not have a weapon-rank progression in this game.'
});
const maxLevel = limits.maxLevel ?? 10000;
if (field === 'prestige' && delta > 0 && limits.canPrestigeOnlyAtMaxLevel) {
const [current] = await db
.select({ level: trackerWeaponRanks.level, prestige: trackerWeaponRanks.prestige })
.from(trackerWeaponRanks)
.where(
and(eq(trackerWeaponRanks.userId, locals.user.id), eq(trackerWeaponRanks.itemId, itemId))
);
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
.insert(trackerWeaponRanks)
.values({
userId: locals.user.id,
itemId,
level: field === 'level' ? Math.max(1, Math.min(maxLevel, 1 + delta)) : 1,
prestige: field === 'prestige' ? Math.max(0, delta) : 0
})
.values({ userId: locals.user.id, itemId, ...stepRank(limits, null, delta) })
.onConflictDoUpdate({
target: [trackerWeaponRanks.userId, trackerWeaponRanks.itemId],
set: {
...(field === 'level'
? {
level: sql`greatest(1, least(${maxLevel}, ${trackerWeaponRanks.level} + ${delta}))`
}
: {
prestige: sql`greatest(0, least(${limits.maxPrestige}, ${trackerWeaponRanks.prestige} + ${delta}))`,
...(delta > 0 ? { level: 1 } : {})
}),
updatedAt: new Date()
},
setWhere:
field === 'prestige' && delta > 0 && limits.canPrestigeOnlyAtMaxLevel
? sql`${trackerWeaponRanks.level} >= ${maxLevel} and ${trackerWeaponRanks.prestige} < ${limits.maxPrestige}`
: undefined
...steppedRank(trackerWeaponRanks.level, trackerWeaponRanks.prestige, limits, delta),
updatedAt
}
})
.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 }) => {