From 6abcb2e65f62e8de906f71ffb1955ec065b5372f Mon Sep 17 00:00:00 2001 From: Kalle <38327916+Sendouc@users.noreply.github.com> Date: Thu, 27 Jun 2024 15:13:05 +0300 Subject: [PATCH] Sub abilities sorting Closes #1313 --- .../builds/core/ability-sorting.server.ts | 162 ++++++++++++++++++ .../builds/queries/buildsBy.server.ts | 3 +- .../loaders/u.$identifier.builds.server.ts | 16 +- 3 files changed, 178 insertions(+), 3 deletions(-) create mode 100644 app/features/builds/core/ability-sorting.server.ts diff --git a/app/features/builds/core/ability-sorting.server.ts b/app/features/builds/core/ability-sorting.server.ts new file mode 100644 index 000000000..0c0469c63 --- /dev/null +++ b/app/features/builds/core/ability-sorting.server.ts @@ -0,0 +1,162 @@ +import { + type Ability, + type BuildAbilitiesTuple, + abilities, +} from "~/modules/in-game-lists"; +import { mainOnlyAbilitiesShort } from "~/modules/in-game-lists/abilities"; + +const abilityToIndex = abilities.reduce( + (acc, ability, index) => { + acc[ability.name] = index; + return acc; + }, + {} as Record, +); + +const mainOnlyAbilitiesSet = new Set(mainOnlyAbilitiesShort); + +const abilitySorter = (a: Ability, b: Ability) => + abilityToIndex[a] - abilityToIndex[b]; + +export function sortAbilities( + abilities: BuildAbilitiesTuple, +): BuildAbilitiesTuple { + const m1 = abilities[0][0]; + const m2 = abilities[1][0]; + const m3 = abilities[2][0]; + const oldMainAbilities = [abilities[0][0], abilities[1][0], abilities[2][0]]; + + const sortedMainAbilities = [m1, m2, m3] + .filter((ability) => !mainOnlyAbilitiesSet.has(ability as any)) + .sort(abilitySorter); + const newMainAbilities = oldMainAbilities.map((ability) => + mainOnlyAbilitiesSet.has(ability as any) + ? ability + : sortedMainAbilities.pop(), + ); + + const subAbilities = subAbilitiesSorted(abilities); + + return switchSubRowsIfBetter([ + [newMainAbilities[0], ...subAbilities.slice(0, 3)], + [newMainAbilities[1], ...subAbilities.slice(3, 6)], + [newMainAbilities[2], ...subAbilities.slice(6, 9)], + ] as BuildAbilitiesTuple); +} + +const sortAbilityCount = (a: [Ability, number], b: [Ability, number]) => { + if (a[1] === b[1]) { + return abilitySorter(a[0], b[0]); + } + + return b[1] - a[1]; +}; +function subAbilitiesSorted(abilities: BuildAbilitiesTuple): Ability[] { + const subAbilitiesUnsorted = [ + abilities[0].slice(1), + abilities[1].slice(1), + abilities[2].slice(1), + ].flat(); + + const counts = Array.from( + subAbilitiesUnsorted + .reduce((acc, cur) => { + if (!acc.has(cur)) { + acc.set(cur, 1); + } else { + acc.set(cur, acc.get(cur)! + 1); + } + return acc; + }, new Map()) + .entries(), + ).sort(sortAbilityCount); + + const subAbilities: Ability[][] = [[], [], []]; + while (counts.length > 0) { + const [ability, count] = counts[0]; + if (count >= 3) { + for (const row of subAbilities) { + if (row.length === 0) { + row.push(ability, ability, ability); + counts[0][1] -= 3; + break; + } + } + } else if (count === 2) { + let found = false; + for (const row of subAbilities) { + if (row.length === 0) { + row.push(ability, ability); + counts[0][1] -= 2; + found = true; + break; + } + } + + if (!found) { + // treat them as singles + counts.push([ability, 1]); + counts.push([ability, 1]); + counts.shift(); + } + } else { + let found = false; + for (const row of subAbilities) { + if (row.length < 3) { + row.push(ability); + counts[0][1] -= 1; + found = true; + break; + } + } + if (!found) { + throw new Error("Absurd state in ability sorting"); + } + } + + if (counts[0][1] === 0) { + counts.shift(); + } + + counts.sort(sortAbilityCount); + } + + return subAbilities.flat(); +} + +function switchSubRowsIfBetter( + abilities: BuildAbilitiesTuple, +): BuildAbilitiesTuple { + const desiredMoves: [source: number, target: number][] = []; + + for (const [i, row] of abilities.entries()) { + const [m, s1] = row; + + // already in a good place + if (m === s1) continue; + + for (const [j, row2] of abilities.entries()) { + if (i === j) continue; + + const [m2, s21] = row2; + + // already a nice row + if (m2 === s21) { + continue; + } + + if (m2 === s1 && !desiredMoves.some(([, target]) => target === j)) { + desiredMoves.push([i, j]); + break; + } + } + } + + for (const [source, target] of desiredMoves) { + const temp = abilities[source].slice(1); + abilities[source].splice(1, 3, ...abilities[target].slice(1)); + abilities[target].splice(1, 3, ...temp); + } + + return abilities; +} diff --git a/app/features/builds/queries/buildsBy.server.ts b/app/features/builds/queries/buildsBy.server.ts index 2274b2f8a..3281247f7 100644 --- a/app/features/builds/queries/buildsBy.server.ts +++ b/app/features/builds/queries/buildsBy.server.ts @@ -11,6 +11,7 @@ import type { MainWeaponId, } from "~/modules/in-game-lists/types"; import invariant from "~/utils/invariant"; +import { sortAbilities } from "../core/ability-sorting.server"; const buildsByWeaponIdStm = sql.prepare(/* sql */ ` with "Top500Weapon" as ( @@ -267,7 +268,7 @@ function augmentBuild({ ...row, modes, weapons, - abilities: dbAbilitiesToArrayOfArrays(abilities), + abilities: sortAbilities(dbAbilitiesToArrayOfArrays(abilities)), }; } diff --git a/app/features/user-page/loaders/u.$identifier.builds.server.ts b/app/features/user-page/loaders/u.$identifier.builds.server.ts index e4d0de297..3aa38c6f4 100644 --- a/app/features/user-page/loaders/u.$identifier.builds.server.ts +++ b/app/features/user-page/loaders/u.$identifier.builds.server.ts @@ -1,6 +1,7 @@ import type { LoaderFunctionArgs } from "@remix-run/node"; import { getUserId } from "~/features/auth/core/user.server"; import * as BuildRepository from "~/features/builds/BuildRepository.server"; +import { sortAbilities } from "~/features/builds/core/ability-sorting.server"; import * as UserRepository from "~/features/user-page/UserRepository.server"; import type { MainWeaponId } from "~/modules/in-game-lists"; import { notFoundIfFalsy, privatelyCachedJson } from "~/utils/remix"; @@ -19,12 +20,23 @@ export const loader = async ({ params, request }: LoaderFunctionArgs) => { showPrivate: loggedInUser?.id === user.id, }); - if (builds.length === 0 && loggedInUser?.id !== user.id) { + const skippedViaSearchParams = + new URL(request.url).searchParams.get("exact") === "true"; + const skipAbilitySorting = + loggedInUser?.id === user.id || skippedViaSearchParams; + const buildsWithAbilitiesSorted = skipAbilitySorting + ? builds + : builds.map((build) => ({ + ...build, + abilities: sortAbilities(build.abilities), + })); + + if (buildsWithAbilitiesSorted.length === 0 && loggedInUser?.id !== user.id) { throw new Response(null, { status: 404 }); } const sortedBuilds = sortBuilds({ - builds, + builds: buildsWithAbilitiesSorted, buildSorting: user.buildSorting, weaponPool: user.weapons, });