sendou.ink/app/features/build-analyzer/analyzer-hooks.ts
Kalle 9cd3beca4d
Some checks are pending
Tests and checks on push / run-checks-and-tests (push) Waiting to run
Updates translation progress / update-translation-progress-issue (push) Waiting to run
Make new build weapon select default to "no weapon" rather than Sploosh
2025-07-24 21:35:58 +03:00

188 lines
4.8 KiB
TypeScript

import { useSearchParams } from "@remix-run/react";
import { abilities } from "~/modules/in-game-lists/abilities";
import type {
Ability,
AbilityWithUnknown,
BuildAbilitiesTupleWithUnknown,
MainWeaponId,
} from "~/modules/in-game-lists/types";
import { isAbility } from "~/modules/in-game-lists/utils";
import invariant from "~/utils/invariant";
import { MAX_LDE_INTENSITY } from "./analyzer-constants";
import type { SpecialEffectType } from "./analyzer-types";
import { applySpecialEffects, SPECIAL_EFFECTS } from "./core/specialEffects";
import { buildStats } from "./core/stats";
import {
buildIsEmpty,
buildToAbilityPoints,
serializeBuild,
validatedBuildFromSearchParams,
validatedWeaponIdFromSearchParams,
} from "./core/utils";
export function useAnalyzeBuild() {
const [searchParams, setSearchParams] = useSearchParams();
const mainWeaponId = validatedWeaponIdFromSearchParams(searchParams) ?? 0;
const build = validatedBuildFromSearchParams(searchParams);
const build2 = validatedBuildFromSearchParams(searchParams, "build2", build);
const ldeIntensity = validatedLdeIntensityFromSearchParams(searchParams);
const effects = validatedEffectsFromSearchParams({ searchParams, build });
const effects2 = validatedEffectsFromSearchParams({
searchParams,
build: build2,
});
const focused = validatedFocusedFromSearchParams({ searchParams });
invariant(
!(buildIsEmpty(build) && !buildIsEmpty(build2)),
"build1 is empty but build2 isn't",
);
const handleChange = ({
newMainWeaponId = mainWeaponId,
newBuild = build,
newBuild2 = build2,
newLdeIntensity = ldeIntensity,
newEffects = effects,
newFocused = focused,
}: {
newMainWeaponId?: MainWeaponId;
newBuild?: BuildAbilitiesTupleWithUnknown;
newBuild2?: BuildAbilitiesTupleWithUnknown;
newLdeIntensity?: number;
newEffects?: Array<SpecialEffectType>;
newFocused?: 1 | 2 | 3;
}) => {
setSearchParams(
{
weapon: String(newMainWeaponId),
build: serializeBuild(newBuild),
build2: serializeBuild(newBuild2),
lde: String(newLdeIntensity),
effect: newEffects,
focused: String(newFocused),
},
{ replace: true, preventScrollReset: true },
);
};
const buildAbilityPoints = buildToAbilityPoints(build);
const abilityPoints = applySpecialEffects({
abilityPoints: buildAbilityPoints,
effects,
ldeIntensity,
});
const hasTacticooler = effects.includes("TACTICOOLER");
const analyzed = buildStats({
abilityPoints,
weaponSplId: mainWeaponId,
mainOnlyAbilities: build
.map((row) => row[0])
.filter(filterMainOnlyAbilities),
hasTacticooler,
});
const buildAbilityPoints2 = buildToAbilityPoints(build2);
const abilityPoints2 = applySpecialEffects({
abilityPoints: buildAbilityPoints2,
effects: effects2,
ldeIntensity,
});
const analyzed2 = buildStats({
abilityPoints: abilityPoints2,
weaponSplId: mainWeaponId,
mainOnlyAbilities: build2
.map((row) => row[0])
.filter(filterMainOnlyAbilities),
hasTacticooler,
});
return {
build,
build2,
focusedBuild: focused === 1 ? build : focused === 2 ? build2 : null,
focused,
mainWeaponId,
handleChange,
analyzed,
analyzed2,
abilityPoints,
abilityPoints2,
allEffects: Array.from(new Set([...effects, ...effects2])),
ldeIntensity,
};
}
function filterMainOnlyAbilities(
ability: AbilityWithUnknown,
): ability is Ability {
const abilityObj = abilities.find((a) => a.name === ability);
return Boolean(abilityObj && abilityObj.type !== "STACKABLE");
}
function validatedLdeIntensityFromSearchParams(searchParams: URLSearchParams) {
const ldeIntensity = searchParams.get("lde")
? Number(searchParams.get("lde"))
: null;
if (
!ldeIntensity ||
!Number.isInteger(ldeIntensity) ||
ldeIntensity < 0 ||
ldeIntensity > MAX_LDE_INTENSITY
) {
return 0;
}
return ldeIntensity;
}
function validatedEffectsFromSearchParams({
searchParams,
build,
}: {
searchParams: URLSearchParams;
build: BuildAbilitiesTupleWithUnknown;
}) {
const result: Array<SpecialEffectType> = [];
const effects = searchParams.getAll("effect");
const effectsNoDuplicates = [...new Set(effects)];
const abilities = build.flat();
for (const effect of effectsNoDuplicates) {
const effectObj = SPECIAL_EFFECTS.find((e) => e.type === effect);
if (!effectObj) continue;
// e.g. even if OG effect is active in state
// it can't be on unless build has OG
if (isAbility(effect) && !abilities.includes(effect)) {
continue;
}
result.push(effect as SpecialEffectType);
}
// lde is a special case in that it's always
// considered active when in the build
if (abilities.includes("LDE") && !result.includes("LDE")) {
result.push("LDE");
}
return result;
}
function validatedFocusedFromSearchParams({
searchParams,
}: {
searchParams: URLSearchParams;
}) {
const focused = searchParams.get("focused");
if (focused === "2") return 2;
if (focused === "3") return 3;
return 1;
}