Splatoon 3 XP badges
|
|
@ -73,3 +73,16 @@ export const CUSTOMIZED_CSS_VARS_NAME = "css";
|
|||
export const MAX_AP = 57;
|
||||
|
||||
export const ONE_HOUR_IN_MS = 60 * 60 * 1000;
|
||||
|
||||
export const SPLATOON_3_XP_BADGE_VALUES = [
|
||||
3400, 3300, 3200, 3100, 3000, 2900, 2800, 2700, 2600,
|
||||
] as const;
|
||||
export const findSplatoon3XpBadgeValue = (xPower: number) => {
|
||||
for (const value of SPLATOON_3_XP_BADGE_VALUES) {
|
||||
if (xPower >= value) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ import forcePatronSql from "./forcePatron.sql";
|
|||
import makeVideoAdderSql from "./makeVideoAdder.sql";
|
||||
import linkPlayerSql from "./linkPlayer.sql";
|
||||
import unlinkPlayerSql from "./unlinkPlayer.sql";
|
||||
import { syncXPBadges } from "~/features/badges";
|
||||
|
||||
const upsertStm = sql.prepare(upsertSql);
|
||||
export function upsert(
|
||||
|
|
@ -279,4 +280,5 @@ export function linkPlayer({
|
|||
}) {
|
||||
unlinkPlayerStm.run({ userId });
|
||||
linkPlayerStm.run({ userId, playerId });
|
||||
syncXPBadges();
|
||||
}
|
||||
|
|
|
|||
1
app/features/badges/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export { syncXPBadges } from "./queries/syncXPBadges.server";
|
||||
60
app/features/badges/queries/syncXPBadges.server.ts
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
import invariant from "tiny-invariant";
|
||||
import {
|
||||
SPLATOON_3_XP_BADGE_VALUES,
|
||||
findSplatoon3XpBadgeValue,
|
||||
} from "~/constants";
|
||||
import { sql } from "~/db/sql";
|
||||
|
||||
const badgeCodeToIdStm = sql.prepare(/* sql */ `
|
||||
select "id"
|
||||
from "Badge"
|
||||
where "code" = @code
|
||||
`);
|
||||
|
||||
const deleteBadgeOwnerStm = sql.prepare(/* sql */ `
|
||||
delete from "TournamentBadgeOwner"
|
||||
where "badgeId" = @badgeId
|
||||
`);
|
||||
|
||||
const userTopXPowersStm = sql.prepare(/* sql */ `
|
||||
select
|
||||
"SplatoonPlayer"."userId",
|
||||
max("XRankPlacement"."power") as "xPower"
|
||||
from
|
||||
"SplatoonPlayer"
|
||||
left join "XRankPlacement" on "XRankPlacement"."playerId" = "SplatoonPlayer"."id"
|
||||
where "SplatoonPlayer"."userId" is not null
|
||||
group by "SplatoonPlayer"."userId"
|
||||
`);
|
||||
|
||||
const addXPBadgeStm = sql.prepare(/* sql */ `
|
||||
insert into "TournamentBadgeOwner" ("badgeId", "userId")
|
||||
values (
|
||||
(select "id" from "Badge" where "code" = @code),
|
||||
@userId
|
||||
)
|
||||
`);
|
||||
|
||||
export const syncXPBadges = sql.transaction(() => {
|
||||
for (const value of SPLATOON_3_XP_BADGE_VALUES) {
|
||||
const badgeId = badgeCodeToIdStm
|
||||
.pluck()
|
||||
.get({ code: String(value) }) as number;
|
||||
|
||||
invariant(badgeId, `Badge ${value} not found`);
|
||||
|
||||
deleteBadgeOwnerStm.run({ badgeId });
|
||||
}
|
||||
|
||||
const userTopXPowers = userTopXPowersStm.all() as Array<{
|
||||
userId: number;
|
||||
xPower: number;
|
||||
}>;
|
||||
|
||||
for (const { userId, xPower } of userTopXPowers) {
|
||||
const badgeValue = findSplatoon3XpBadgeValue(xPower);
|
||||
if (!badgeValue) continue;
|
||||
|
||||
addXPBadgeStm.run({ code: String(badgeValue), userId });
|
||||
}
|
||||
});
|
||||
|
|
@ -18,6 +18,7 @@ import { BADGES_PAGE } from "~/utils/urls";
|
|||
import { type BadgesLoaderData } from "../badges";
|
||||
import { type TFunction } from "react-i18next";
|
||||
import { useTranslation } from "~/hooks/useTranslation";
|
||||
import { SPLATOON_3_XP_BADGE_VALUES } from "~/constants";
|
||||
|
||||
export interface BadgeDetailsContext {
|
||||
badgeName: string;
|
||||
|
|
@ -104,7 +105,10 @@ export function badgeExplanationText(
|
|||
if (badge.code === "patreon_plus") {
|
||||
return t("patreon+");
|
||||
}
|
||||
if (badge.code.startsWith("xp")) {
|
||||
if (
|
||||
badge.code.startsWith("xp") ||
|
||||
SPLATOON_3_XP_BADGE_VALUES.includes(Number(badge.code) as any)
|
||||
) {
|
||||
return t("xp", { xpText: badge.displayName });
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@
|
|||
"replace-weapon-names": "node --experimental-specifier-resolution=node --loader ts-node/esm -r tsconfig-paths/register scripts/replace-weapon-names.ts",
|
||||
"placements": "node --experimental-specifier-resolution=node --loader ts-node/esm -r tsconfig-paths/register scripts/placements/index.ts",
|
||||
"hex-to-filter": "node --experimental-specifier-resolution=node --loader ts-node/esm -r tsconfig-paths/register scripts/hex-to-filter.ts",
|
||||
"sync-xp-badges": "node --experimental-specifier-resolution=node --loader ts-node/esm -r tsconfig-paths/register scripts/sync-xp-badges.ts",
|
||||
"lint:ts": "eslint . --ext .ts,.tsx",
|
||||
"lint:css": "stylelint \"app/styles/**/*.css\"",
|
||||
"prettier:check": "prettier --check . --loglevel warn",
|
||||
|
|
|
|||
BIN
public/static-assets/badges/2600.avif
Normal file
BIN
public/static-assets/badges/2600.gif
Normal file
|
After Width: | Height: | Size: 201 KiB |
BIN
public/static-assets/badges/2600.png
Normal file
|
After Width: | Height: | Size: 3.7 KiB |
BIN
public/static-assets/badges/2700.avif
Normal file
BIN
public/static-assets/badges/2700.gif
Normal file
|
After Width: | Height: | Size: 198 KiB |
BIN
public/static-assets/badges/2700.png
Normal file
|
After Width: | Height: | Size: 3.6 KiB |
BIN
public/static-assets/badges/2800.avif
Normal file
BIN
public/static-assets/badges/2800.gif
Normal file
|
After Width: | Height: | Size: 203 KiB |
BIN
public/static-assets/badges/2800.png
Normal file
|
After Width: | Height: | Size: 3.8 KiB |
BIN
public/static-assets/badges/2900.avif
Normal file
BIN
public/static-assets/badges/2900.gif
Normal file
|
After Width: | Height: | Size: 202 KiB |
BIN
public/static-assets/badges/2900.png
Normal file
|
After Width: | Height: | Size: 3.8 KiB |
BIN
public/static-assets/badges/3000.avif
Normal file
BIN
public/static-assets/badges/3000.gif
Normal file
|
After Width: | Height: | Size: 232 KiB |
BIN
public/static-assets/badges/3000.png
Normal file
|
After Width: | Height: | Size: 4.3 KiB |
BIN
public/static-assets/badges/3100.avif
Normal file
BIN
public/static-assets/badges/3100.gif
Normal file
|
After Width: | Height: | Size: 216 KiB |
BIN
public/static-assets/badges/3100.png
Normal file
|
After Width: | Height: | Size: 4.0 KiB |
BIN
public/static-assets/badges/3200.avif
Normal file
BIN
public/static-assets/badges/3200.gif
Normal file
|
After Width: | Height: | Size: 225 KiB |
BIN
public/static-assets/badges/3200.png
Normal file
|
After Width: | Height: | Size: 4.0 KiB |
BIN
public/static-assets/badges/3300.avif
Normal file
BIN
public/static-assets/badges/3300.gif
Normal file
|
After Width: | Height: | Size: 227 KiB |
BIN
public/static-assets/badges/3300.png
Normal file
|
After Width: | Height: | Size: 4.2 KiB |
BIN
public/static-assets/badges/3400.avif
Normal file
BIN
public/static-assets/badges/3400.gif
Normal file
|
After Width: | Height: | Size: 226 KiB |
BIN
public/static-assets/badges/3400.png
Normal file
|
After Width: | Height: | Size: 4.2 KiB |
|
|
@ -6,6 +6,7 @@ import { sql } from "~/db/sql";
|
|||
import type { XRankPlacement } from "~/db/types";
|
||||
import { type MainWeaponId, mainWeaponIds } from "~/modules/in-game-lists";
|
||||
import { xRankSchema } from "./schemas";
|
||||
import { syncXPBadges } from "~/features/badges";
|
||||
|
||||
const rawJsonNumber = process.argv[2]?.trim();
|
||||
invariant(rawJsonNumber, "jsonNumber is required (argument 1)");
|
||||
|
|
@ -50,6 +51,7 @@ async function main() {
|
|||
}
|
||||
|
||||
addPlacements(placements);
|
||||
syncXPBadges();
|
||||
console.log(`done reading in ${placements.length} placements`);
|
||||
}
|
||||
|
||||
|
|
|
|||
7
scripts/sync-xp-badges.ts
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
/* eslint-disable no-console */
|
||||
import "dotenv/config";
|
||||
import { syncXPBadges } from "~/features/badges";
|
||||
|
||||
syncXPBadges();
|
||||
|
||||
console.log("Synced XP badges");
|
||||