mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-08-27 13:45:27 -05:00
EffectsSelector with LDE dropdown
This commit is contained in:
2
app/modules/analyzer/constants.ts
Normal file
2
app/modules/analyzer/constants.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export const MAX_LDE_INTENSITY = 21;
|
||||
export const MAX_AP = 57;
|
||||
@@ -7,3 +7,7 @@ export type {
|
||||
} from "./types";
|
||||
|
||||
export { useAnalyzeBuild } from "./useAnalyzeBuild";
|
||||
|
||||
export { MAX_LDE_INTENSITY } from "./constants";
|
||||
|
||||
export { lastDitchEffortIntensityToAp } from "./specialEffects";
|
||||
|
||||
@@ -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 [
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 <Main>Coming soon :)</Main>;
|
||||
@@ -64,11 +78,20 @@ export default function BuildAnalyzerPage() {
|
||||
</div>
|
||||
<WeaponInfoBadges analyzed={analyzed} />
|
||||
</div>
|
||||
<AbilitiesSelector
|
||||
selectedAbilities={build}
|
||||
onChange={(newBuild) => handleChange({ newBuild })}
|
||||
/>
|
||||
<AbilityPointsDetails abilityPoints={abilityPoints} />
|
||||
<div className="stack md items-center">
|
||||
<AbilitiesSelector
|
||||
selectedAbilities={build}
|
||||
onChange={(newBuild) => handleChange({ newBuild })}
|
||||
/>
|
||||
<EffectsSelector
|
||||
build={build}
|
||||
ldeIntensity={ldeIntensity}
|
||||
handleLdeIntensityChange={(newLdeIntensity) =>
|
||||
handleChange({ newLdeIntensity })
|
||||
}
|
||||
/>
|
||||
<AbilityPointsDetails abilityPoints={abilityPoints} />
|
||||
</div>
|
||||
<div className="analyzer__patch">
|
||||
{t("analyzer:patch")} {CURRENT_PATCH}
|
||||
</div>
|
||||
@@ -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 (
|
||||
<div className="analyzer__effects-selector">
|
||||
{effectsToShow.map((effect) => {
|
||||
return (
|
||||
<React.Fragment key={effect.type}>
|
||||
<div>
|
||||
{isAbility(effect.type) ? (
|
||||
<Ability ability={effect.type} size="SUB" />
|
||||
) : (
|
||||
<Image
|
||||
path={specialWeaponImageUrl(15)}
|
||||
alt={t("weapons:SPECIAL_15")}
|
||||
height={32}
|
||||
width={32}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
{effect.type === "LDE" ? (
|
||||
<select
|
||||
value={ldeIntensity}
|
||||
onChange={(e) =>
|
||||
handleLdeIntensityChange(Number(e.target.value))
|
||||
}
|
||||
className="analyzer__lde-intensity-select"
|
||||
>
|
||||
{new Array(MAX_LDE_INTENSITY + 1).fill(null).map((_, i) => {
|
||||
const percentage = ((i / MAX_LDE_INTENSITY) * 100)
|
||||
.toFixed(2)
|
||||
.replace(".00", "");
|
||||
|
||||
return (
|
||||
<option key={i} value={i}>
|
||||
{percentage}% (+{lastDitchEffortIntensityToAp(i)}{" "}
|
||||
{t("analyzer:abilityPoints.short")})
|
||||
</option>
|
||||
);
|
||||
})}
|
||||
</select>
|
||||
) : (
|
||||
<div />
|
||||
)}
|
||||
</div>
|
||||
</React.Fragment>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function AbilityPointsDetails({
|
||||
abilityPoints,
|
||||
}: {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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}}."
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user