diff --git a/app/modules/analyzer/constants.ts b/app/modules/analyzer/constants.ts new file mode 100644 index 000000000..89b7e959a --- /dev/null +++ b/app/modules/analyzer/constants.ts @@ -0,0 +1,2 @@ +export const MAX_LDE_INTENSITY = 21; +export const MAX_AP = 57; diff --git a/app/modules/analyzer/index.ts b/app/modules/analyzer/index.ts index 247c404c2..f1c0d9ea8 100644 --- a/app/modules/analyzer/index.ts +++ b/app/modules/analyzer/index.ts @@ -7,3 +7,7 @@ export type { } from "./types"; export { useAnalyzeBuild } from "./useAnalyzeBuild"; + +export { MAX_LDE_INTENSITY } from "./constants"; + +export { lastDitchEffortIntensityToAp } from "./specialEffects"; diff --git a/app/modules/analyzer/specialEffects.ts b/app/modules/analyzer/specialEffects.ts index 94b555003..90a48678a 100644 --- a/app/modules/analyzer/specialEffects.ts +++ b/app/modules/analyzer/specialEffects.ts @@ -1,8 +1,24 @@ +import { MAX_AP } from "./constants"; import type { AbilityPoints } from "./types"; -const MAX_AP = 57; - export const SPECIAL_EFFECTS = [ + { + type: "DR", + values: [ + { + type: "SSU", + ap: 10, + }, + { + type: "RSU", + ap: 10, + }, + { + type: "RES", + ap: 10, + }, + ], + }, { type: "OG", values: [ @@ -53,23 +69,6 @@ export const SPECIAL_EFFECTS = [ }, ], }, - { - type: "DR", - values: [ - { - type: "SSU", - ap: 10, - }, - { - type: "RSU", - ap: 10, - }, - { - type: "RES", - ap: 10, - }, - ], - }, { type: "TACTICOOLER", values: [ @@ -107,8 +106,12 @@ export const SPECIAL_EFFECTS = [ }, ] as const; +export function lastDitchEffortIntensityToAp(intensity: number) { + return Math.floor((24 / 21) * intensity); +} + function lastDitchEffortValues(intensity: number) { - const ap = Math.floor((24 / 21) * intensity); + const ap = lastDitchEffortIntensityToAp(intensity); return [ { diff --git a/app/modules/analyzer/useAnalyzeBuild.ts b/app/modules/analyzer/useAnalyzeBuild.ts index 4d7deed30..472f079dc 100644 --- a/app/modules/analyzer/useAnalyzeBuild.ts +++ b/app/modules/analyzer/useAnalyzeBuild.ts @@ -9,6 +9,7 @@ import { isAbility, } from "../in-game-lists"; import type { AbilityType, AbilityWithUnknown } from "../in-game-lists/types"; +import { MAX_LDE_INTENSITY } from "./constants"; import { applySpecialEffects, SPECIAL_EFFECTS } from "./specialEffects"; import { buildStats } from "./stats"; import type { SpecialEffectType } from "./types"; @@ -68,6 +69,7 @@ export function useAnalyzeBuild() { handleChange, analyzed, abilityPoints, + ldeIntensity, }; } @@ -142,7 +144,6 @@ function validateAbility( throw new Error("Invalid ability"); } -const MAX_LDE_INTENSITY = 21; function validatedLdeIntensityFromSearchParams(searchParams: URLSearchParams) { const ldeIntensity = searchParams.get("lde") ? Number(searchParams.get("lde")) @@ -171,6 +172,7 @@ function validatedEffectsFromSearchParams({ 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); @@ -178,12 +180,18 @@ function validatedEffectsFromSearchParams({ // e.g. even if OG effect is active in state // it can't be on unless build has OG - if (isAbility(effect) && !build.flat().includes(effect)) { + 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; } diff --git a/app/routes/analyzer.tsx b/app/routes/analyzer.tsx index b0e482e9d..0d767c9bc 100644 --- a/app/routes/analyzer.tsx +++ b/app/routes/analyzer.tsx @@ -1,4 +1,5 @@ import { type MetaFunction, type LinksFunction } from "@remix-run/node"; +import * as React from "react"; import { useTranslation } from "react-i18next"; import { AbilitiesSelector } from "~/components/AbilitiesSelector"; import { Ability } from "~/components/Ability"; @@ -7,10 +8,17 @@ import { Image } from "~/components/Image"; import { Main } from "~/components/Main"; import { useSetTitle } from "~/hooks/useSetTitle"; import type { AnalyzedBuild, Stat } from "~/modules/analyzer"; +import { MAX_LDE_INTENSITY } from "~/modules/analyzer"; import { useAnalyzeBuild } from "~/modules/analyzer"; +import { + lastDitchEffortIntensityToAp, + SPECIAL_EFFECTS, +} from "~/modules/analyzer/specialEffects"; import type { AbilityPoints } from "~/modules/analyzer/types"; +import type { BuildAbilitiesTupleWithUnknown } from "~/modules/in-game-lists"; import { abilities, + isAbility, type MainWeaponId, type SubWeaponId, } from "~/modules/in-game-lists"; @@ -37,8 +45,14 @@ export const handle = { export default function BuildAnalyzerPage() { const { t } = useTranslation(["analyzer", "common"]); useSetTitle(t("common:pages.buildAnalyzer")); - const { build, mainWeaponId, handleChange, analyzed, abilityPoints } = - useAnalyzeBuild(); + const { + build, + mainWeaponId, + handleChange, + analyzed, + abilityPoints, + ldeIntensity, + } = useAnalyzeBuild(); // xxx: remove before prod if (process.env.NODE_ENV === "production") return
Coming soon :)
; @@ -64,11 +78,20 @@ export default function BuildAnalyzerPage() { - handleChange({ newBuild })} - /> - +
+ handleChange({ newBuild })} + /> + + handleChange({ newLdeIntensity }) + } + /> + +
{t("analyzer:patch")} {CURRENT_PATCH}
@@ -233,6 +256,71 @@ function WeaponInfoBadges({ analyzed }: { analyzed: AnalyzedBuild }) { ); } +function EffectsSelector({ + build, + ldeIntensity, + handleLdeIntensityChange, +}: { + build: BuildAbilitiesTupleWithUnknown; + ldeIntensity: number; + handleLdeIntensityChange: (newLdeIntensity: number) => void; +}) { + const { t } = useTranslation(["weapons", "analyzer"]); + + const effectsToShow = SPECIAL_EFFECTS.filter( + (effect) => !isAbility(effect.type) || build.flat().includes(effect.type) + ).reverse(); // reverse to show Tacticooler first as it always shows + + return ( +
+ {effectsToShow.map((effect) => { + return ( + +
+ {isAbility(effect.type) ? ( + + ) : ( + {t("weapons:SPECIAL_15")} + )} +
+
+ {effect.type === "LDE" ? ( + + ) : ( +
+ )} +
+ + ); + })} +
+ ); +} + function AbilityPointsDetails({ abilityPoints, }: { diff --git a/app/styles/analyzer.css b/app/styles/analyzer.css index f8179d1ad..7e473754a 100644 --- a/app/styles/analyzer.css +++ b/app/styles/analyzer.css @@ -34,19 +34,30 @@ padding-inline: var(--s-1-5); } +.analyzer__effects-selector { + display: grid; + gap: var(--s-2); + grid-template-columns: 1fr 2.5fr; + place-items: center; +} + +.analyzer__lde-intensity-select { + font-size: var(--fonts-xxs); +} + .analyzer__ap-summary { + width: 100%; background-color: var(--bg-lighter); border-radius: var(--rounded); font-size: var(--fonts-xxs); font-weight: var(--semi-bold); - width: 100%; padding-block: var(--s-1); padding-inline: var(--s-2); } .analyzer__ap-text { - font-size: var(--fonts-xxs); color: var(--text-lighter); + font-size: var(--fonts-xxs); font-weight: var(--semi-bold); } diff --git a/public/locales/en/analyzer.json b/public/locales/en/analyzer.json index a8e9c8880..428345bfa 100644 --- a/public/locales/en/analyzer.json +++ b/public/locales/en/analyzer.json @@ -54,5 +54,6 @@ "build": "Build", "patch": "Patch:", "abilityPoints": "Ability points", + "abilityPoints.short": "AP", "consumptionExplanation": "This chart shows the amount of actions left to perform with main weapon after using sub 0-{{maxSubsToUse}} times. Max amount of consecutive subs to use with full ink tank is {{maxSubsToUse}}." }