sendou.ink/app/features/leaderboards/core/leaderboards.server.ts

173 lines
4.8 KiB
TypeScript

import { cachified } from "@epic-web/cachified";
import * as Seasons from "~/features/mmr/core/Seasons";
import { USER_LEADERBOARD_MIN_ENTRIES_FOR_LEVIATHAN } from "~/features/mmr/mmr-constants";
import { freshUserSkills, userSkills } from "~/features/mmr/tiered.server";
import * as UserRepository from "~/features/user-page/UserRepository.server";
import type { MainWeaponId } from "~/modules/in-game-lists/types";
import { weaponCategories } from "~/modules/in-game-lists/weapon-ids";
import { cache, IN_MILLISECONDS, ttl } from "~/utils/cache.server";
import type { Unwrapped } from "~/utils/types";
import { DEFAULT_LEADERBOARD_MAX_SIZE } from "../leaderboards-constants";
import { seasonHasTopTen } from "../leaderboards-utils";
import type { SeasonPopularUsersWeapon } from "../queries/seasonPopularUsersWeapon.server";
import { seasonPopularUsersWeapon } from "../queries/seasonPopularUsersWeapon.server";
import type { UserSPLeaderboardItem } from "../queries/userSPLeaderboard.server";
import { userSPLeaderboard } from "../queries/userSPLeaderboard.server";
export type UserLeaderboardWithAdditionsItem = Unwrapped<
typeof cachedFullUserLeaderboard
>;
export async function cachedFullUserLeaderboard(season: number) {
return cachified({
key: `user-leaderboard-season-${season}`,
cache,
ttl: ttl(IN_MILLISECONDS.HALF_HOUR),
async getFreshValue() {
const leaderboard = userSPLeaderboard(season);
const withTiers = addTiers(leaderboard, season);
const shouldAddPendingPlusTier =
season === Seasons.current()?.nth &&
leaderboard.length >= USER_LEADERBOARD_MIN_ENTRIES_FOR_LEVIATHAN;
const withPendingPlusTiers = shouldAddPendingPlusTier
? addPendingPlusTiers(
withTiers,
await UserRepository.findAllPlusServerMembers(),
season,
)
: withTiers;
return addWeapons(withPendingPlusTiers, seasonPopularUsersWeapon(season));
},
});
}
function addTiers(entries: UserSPLeaderboardItem[], season: number) {
const tiers = freshUserSkills(season);
const encounteredTiers = new Set<string>();
return entries.map((entry, i) => {
const tier = tiers.userSkills[entry.id].tier;
if (i < 10 && seasonHasTopTen(season)) {
return { ...entry, tier, firstOfTier: undefined };
}
const tierKey = `${tier.name}${tier.isPlus ? "+" : ""}`;
const tierAlreadyEncountered = encounteredTiers.has(tierKey);
if (!tierAlreadyEncountered) {
encounteredTiers.add(tierKey);
}
return {
...entry,
tier,
firstOfTier: !tierAlreadyEncountered ? tier : undefined,
};
});
}
const PLUS_TIER_QUOTA = {
"+1": 5,
"+2": 10,
"+3": 15,
} as const;
export function addPendingPlusTiers<T extends UserSPLeaderboardItem>(
entries: T[],
plusTiers: Array<{
userId: number;
plusTier: number;
}>,
seasonNth: number,
) {
const quota: { "+1": number; "+2": number; "+3": number } = {
...PLUS_TIER_QUOTA,
};
const resolveHighestPlusTierWithSpace = () => {
if (quota["+1"] > 0) return 1;
if (quota["+2"] > 0) return 2;
if (quota["+3"] > 0) return 3;
return null;
};
for (const entry of entries) {
const highestPlusTierWithSpace = resolveHighestPlusTierWithSpace();
if (!highestPlusTierWithSpace) break;
const plusTier = plusTiers.find((t) => t.userId === entry.id)?.plusTier;
if (plusTier && plusTier <= highestPlusTierWithSpace) continue;
if (entry.plusSkippedForSeasonNth === seasonNth) {
entry.plusSkippedForSeasonNth = null;
continue;
}
entry.pendingPlusTier = highestPlusTierWithSpace;
const key = `+${highestPlusTierWithSpace}` as const;
quota[key] -= 1;
}
return entries;
}
function addWeapons<T extends { id: number }>(
entries: T[],
weapons: SeasonPopularUsersWeapon,
) {
return entries.map((entry) => {
const weaponSplId = weapons[entry.id] as MainWeaponId | undefined;
return {
...entry,
weaponSplId,
};
});
}
export function filterByWeaponCategory<
T extends { weaponSplId?: MainWeaponId },
>(entries: Array<T>, category: (typeof weaponCategories)[number]["name"]) {
const weaponIdsOfCategory = new Set(
weaponCategories.find((c) => c.name === category)!.weaponIds,
);
return entries.filter(
(entry) => entry.weaponSplId && weaponIdsOfCategory.has(entry.weaponSplId),
);
}
export function ownEntryPeek({
leaderboard,
userId,
season,
}: {
leaderboard: UserSPLeaderboardItem[];
userId: number;
season: number;
}) {
const found = leaderboard.find(
(entry) =>
entry.id === userId && entry.placementRank > DEFAULT_LEADERBOARD_MAX_SIZE,
);
if (!found) return null;
const withTier = addTiers([found], season)[0];
const { intervals } = userSkills(season);
const currentTierIndex = intervals.findIndex(
(interval) =>
interval.name === withTier.tier.name &&
interval.isPlus === withTier.tier.isPlus,
);
const nextTier =
currentTierIndex > 0 ? intervals[currentTierIndex - 1] : undefined;
return {
entry: withTier,
nextTier,
};
}