diff --git a/frontend-react/package-lock.json b/frontend-react/package-lock.json index 26578f0dd..76d7acf7d 100644 --- a/frontend-react/package-lock.json +++ b/frontend-react/package-lock.json @@ -2168,9 +2168,9 @@ } }, "@testing-library/user-event": { - "version": "10.3.4", - "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-10.3.4.tgz", - "integrity": "sha512-ZuqR2cBAOArvBdPiIjmAKaZdFAtZSH6Oz2I5+uHonMXRyR0uTtz2fTWyPuiu4JATdK7gLIq1s02BMJGWgmh1lA==", + "version": "10.3.5", + "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-10.3.5.tgz", + "integrity": "sha512-9Jg5pg12099EoOb4Z/+UNtCFpFIblD6INkgeWuV/AjUiYP+s/u6aYGPaLrUYioJ8duQ3V421B29GVE/3JELjCQ==", "requires": { "@babel/runtime": "^7.9.6" }, diff --git a/frontend-react/package.json b/frontend-react/package.json index 0edef2fe0..913905e91 100644 --- a/frontend-react/package.json +++ b/frontend-react/package.json @@ -12,7 +12,7 @@ "@sendou/react-sketch": "^0.5.2", "@testing-library/jest-dom": "^5.8.0", "@testing-library/react": "^10.0.4", - "@testing-library/user-event": "^10.3.4", + "@testing-library/user-event": "^10.3.5", "@types/jest": "^25.2.3", "@types/reach__router": "^1.3.5", "@types/react": "^16.9.35", diff --git a/frontend-react/src/components/analyzer/BuildAnalyzerPage.tsx b/frontend-react/src/components/analyzer/BuildAnalyzerPage.tsx index 9b107b9cc..b74d791d7 100644 --- a/frontend-react/src/components/analyzer/BuildAnalyzerPage.tsx +++ b/frontend-react/src/components/analyzer/BuildAnalyzerPage.tsx @@ -7,19 +7,26 @@ import { Build } from "../../types" import PageHeader from "../common/PageHeader" import WeaponSelector from "../common/WeaponSelector" import BuildStats from "./BuildStats" -import EditableBuild from "./EditableBuild" +import EditableBuilds from "./EditableBuilds" import MyThemeContext from "../../themeContext" +const defaultBuild: Partial = { + weapon: "Splattershot Jr.", + headgear: ["UNKNOWN", "UNKNOWN", "UNKNOWN", "UNKNOWN"], + clothing: ["UNKNOWN", "UNKNOWN", "UNKNOWN", "UNKNOWN"], + shoes: ["UNKNOWN", "UNKNOWN", "UNKNOWN", "UNKNOWN"], +} + const BuildAnalyzerPage: React.FC = () => { const { themeColor } = useContext(MyThemeContext) - const [build, setBuild] = useState>({ - weapon: "Splattershot Jr.", - headgear: ["UNKNOWN", "UNKNOWN", "UNKNOWN", "UNKNOWN"], - clothing: ["UNKNOWN", "UNKNOWN", "UNKNOWN", "UNKNOWN"], - shoes: ["UNKNOWN", "UNKNOWN", "UNKNOWN", "UNKNOWN"], - }) - const [hideExtra, setHideExtra] = useState(true) + const [build, setBuild] = useState>(defaultBuild) + const [otherBuild, setOtherBuild] = useState>(defaultBuild) + const [showOther, setShowOther] = useState(false) + const [otherFocused, setOtherFocused] = useState(false) + const [hideExtra, setHideExtra] = useState(false) + const explanations = useAbilityEffects(build) + const otherExplanations = useAbilityEffects(otherBuild) console.log("explanations", explanations) @@ -34,11 +41,24 @@ const BuildAnalyzerPage: React.FC = () => { setBuild({ ...build, weapon })} + setValue={(weapon) => { + setBuild({ ...build, weapon }) + setOtherBuild({ ...otherBuild, weapon }) + }} menuIsOpen={!build.weapon} /> - + { + setOtherFocused(!otherFocused) + }} + otherFocused={otherFocused} + /> Hide stats at base value = () => { diff --git a/frontend-react/src/components/analyzer/BuildStats.tsx b/frontend-react/src/components/analyzer/BuildStats.tsx index 848169c4e..933e70f71 100644 --- a/frontend-react/src/components/analyzer/BuildStats.tsx +++ b/frontend-react/src/components/analyzer/BuildStats.tsx @@ -1,23 +1,44 @@ -import React, { useContext } from "react" +import React, { useContext, useState } from "react" import { Explanation } from "../../hooks/useAbilityEffects" import MyThemeContext from "../../themeContext" import { Ability, Build } from "../../types" import { mainOnlyAbilities } from "../../utils/lists" -import { Progress, Box, Flex } from "@chakra-ui/core" +import { + Progress, + Box, + Flex, + Popover, + PopoverTrigger, + PopoverContent, +} from "@chakra-ui/core" import AbilityIcon from "../builds/AbilityIcon" +import { + LineChart, + CartesianGrid, + XAxis, + YAxis, + Legend, + Tooltip, + Line, + ResponsiveContainer, +} from "recharts" +import IconButton from "../elements/IconButton" +import { FaChartLine, FaInfo, FaQuestion } from "react-icons/fa" interface BuildStatsProps { explanations: Explanation[] + otherExplanations?: Explanation[] build: Partial hideExtra: boolean } const BuildStats: React.FC = ({ explanations, + otherExplanations, build, hideExtra, }) => { - const { themeColor, themeColorWithShade } = useContext(MyThemeContext) + const { colorMode, themeColorWithShade } = useContext(MyThemeContext) const abilityArrays: Ability[][] = [ build.headgear ?? [], build.clothing ?? [], @@ -39,11 +60,37 @@ const BuildStats: React.FC = ({ ) const BuildStat: React.FC<{ - title: String + title: string effect: string + otherEffect?: string ability: Ability + info?: string progressBarValue: number - }> = ({ title, effect, ability, progressBarValue = 0 }) => { + otherProgressBarValue?: number + getEffect?: (ap: number) => number + }> = ({ + title, + effect, + ability, + otherEffect, + otherProgressBarValue, + getEffect, + info, + progressBarValue = 0, + }) => { + const [showChart, setShowChart] = useState(false) + const { themeColorHex, darkerBgColor, themeColorWithShade } = useContext( + MyThemeContext + ) + + const getData = () => { + const toReturn = [] + for (let i = 0; i < 58; i++) { + toReturn.push({ name: `${i}AP`, [title]: getEffect!(i) }) + } + + return toReturn + } return ( <> @@ -51,43 +98,136 @@ const BuildStats: React.FC = ({ + setShowChart(!showChart)} + /> {title} + {info && ( + + + + + + + + {info} + + + )} {effect} + {otherEffect && ( + <> + + + + + {otherEffect} + + + + )} + {getEffect && showChart && ( + + + + + + + + + + + + + )} ) } - const explanationsToUse = !hideExtra + /*const explanationsToUse = !hideExtra ? explanations - : explanations.filter((e) => e.effectFromMax !== 0) + : explanations.filter((e) => e.effectFromMax !== 0)*/ return ( <> - {explanationsToUse.map((explanation) => ( - - - - ))} + {explanations.map((_explanation, index) => { + const explanation = explanations[index] + const otherExplanation = otherExplanations + ? otherExplanations[index] + : undefined + + if ( + explanation.effectFromMax === 0 && + (!otherExplanation || otherExplanation.effectFromMax === 0) && + hideExtra + ) { + return null + } + + return ( + + + + ) + })} ) } diff --git a/frontend-react/src/components/analyzer/EditableBuild.tsx b/frontend-react/src/components/analyzer/EditableBuild.tsx deleted file mode 100644 index 45cc36a36..000000000 --- a/frontend-react/src/components/analyzer/EditableBuild.tsx +++ /dev/null @@ -1,125 +0,0 @@ -import React from "react" -import AbilityButtons from "../user/AbilityButtons" -import ViewSlots from "../builds/ViewSlots" -import { Box, Flex } from "@chakra-ui/core" -import { - Build, - Ability, - HeadOnlyAbility, - ClothingOnlyAbility, - ShoesOnlyAbility, - StackableAbility, -} from "../../types" -import { - headOnlyAbilities, - clothingOnlyAbilities, - shoesOnlyAbilities, -} from "../../utils/lists" -import { Explanation } from "../../hooks/useAbilityEffects" - -interface EditableBuildProps { - build: Partial - setBuild: React.Dispatch>> -} - -const EditableBuild: React.FC = ({ build, setBuild }) => { - const handleChange = (value: Object) => setBuild({ ...build, ...value }) - - const handleAbilityButtonClick = (ability: Ability) => { - if (headOnlyAbilities.indexOf(ability as any) !== -1) { - if (build.headgear![0] === "UNKNOWN") { - handleChange({ - headgear: [ - ability, - build.headgear![1], - build.headgear![2], - build.headgear![3], - ], - }) - } - } else if (clothingOnlyAbilities.indexOf(ability as any) !== -1) { - if (build.clothing![0] === "UNKNOWN") { - handleChange({ - clothing: [ - ability, - build.clothing![1], - build.clothing![2], - build.clothing![3], - ], - }) - } - } else if (shoesOnlyAbilities.indexOf(ability as any) !== -1) { - if (build.shoes![0] === "UNKNOWN") { - handleChange({ - shoes: [ability, build.shoes![1], build.shoes![2], build.shoes![3]], - }) - } - } else { - const headI = build.headgear!.indexOf("UNKNOWN") - if (headI !== -1) { - const copy = build.headgear!.slice() - copy[headI] = ability as HeadOnlyAbility | StackableAbility - handleChange({ - headgear: copy, - }) - return - } - - const clothingI = build.clothing!.indexOf("UNKNOWN") - if (clothingI !== -1) { - const copy = build.clothing!.slice() - copy[clothingI] = ability as ClothingOnlyAbility | StackableAbility - handleChange({ - clothing: copy, - }) - return - } - - const shoesI = build.shoes!.indexOf("UNKNOWN") - if (shoesI !== -1) { - const copy = build.shoes!.slice() - copy[shoesI] = ability as ShoesOnlyAbility | StackableAbility - handleChange({ - shoes: copy, - }) - } - } - } - - const handleClickBuildAbility = ( - slot: "HEAD" | "CLOTHING" | "SHOES", - index: number - ) => { - if (slot === "HEAD") { - const copy = build.headgear!.slice() - copy[index] = "UNKNOWN" - handleChange({ - headgear: copy, - }) - } else if (slot === "CLOTHING") { - const copy = build.clothing!.slice() - copy[index] = "UNKNOWN" - handleChange({ - clothing: copy, - }) - } else { - const copy = build.shoes!.slice() - copy[index] = "UNKNOWN" - handleChange({ - shoes: copy, - }) - } - } - return ( - <> - - - handleAbilityButtonClick(ability)} - /> - - - ) -} - -export default EditableBuild diff --git a/frontend-react/src/components/analyzer/EditableBuilds.tsx b/frontend-react/src/components/analyzer/EditableBuilds.tsx new file mode 100644 index 000000000..a99ac6886 --- /dev/null +++ b/frontend-react/src/components/analyzer/EditableBuilds.tsx @@ -0,0 +1,189 @@ +import React from "react" +import AbilityButtons from "../user/AbilityButtons" +import ViewSlots from "../builds/ViewSlots" +import { Box, Flex } from "@chakra-ui/core" +import { + Build, + Ability, + HeadOnlyAbility, + ClothingOnlyAbility, + ShoesOnlyAbility, + StackableAbility, +} from "../../types" +import { + headOnlyAbilities, + clothingOnlyAbilities, + shoesOnlyAbilities, +} from "../../utils/lists" +import Button from "../elements/Button" +import { FaPlus, FaMinus } from "react-icons/fa" + +interface EditableBuildsProps { + build: Partial + otherBuild: Partial + setBuild: React.Dispatch>> + showOther: boolean + setShowOther: React.Dispatch> + otherFocused: boolean + changeFocus: () => void +} + +const EditableBuilds: React.FC = ({ + build, + otherBuild, + setBuild, + showOther, + setShowOther, + otherFocused, + changeFocus, +}) => { + const buildToEdit = otherFocused ? otherBuild : build + const handleChange = (value: Object) => setBuild({ ...buildToEdit, ...value }) + + const handleAbilityButtonClick = (ability: Ability) => { + if (headOnlyAbilities.indexOf(ability as any) !== -1) { + if (buildToEdit.headgear![0] === "UNKNOWN") { + handleChange({ + headgear: [ + ability, + buildToEdit.headgear![1], + buildToEdit.headgear![2], + buildToEdit.headgear![3], + ], + }) + } + } else if (clothingOnlyAbilities.indexOf(ability as any) !== -1) { + if (buildToEdit.clothing![0] === "UNKNOWN") { + handleChange({ + clothing: [ + ability, + buildToEdit.clothing![1], + buildToEdit.clothing![2], + buildToEdit.clothing![3], + ], + }) + } + } else if (shoesOnlyAbilities.indexOf(ability as any) !== -1) { + if (buildToEdit.shoes![0] === "UNKNOWN") { + handleChange({ + shoes: [ + ability, + buildToEdit.shoes![1], + buildToEdit.shoes![2], + buildToEdit.shoes![3], + ], + }) + } + } else { + const headI = buildToEdit.headgear!.indexOf("UNKNOWN") + if (headI !== -1) { + const copy = buildToEdit.headgear!.slice() + copy[headI] = ability as HeadOnlyAbility | StackableAbility + handleChange({ + headgear: copy, + }) + return + } + + const clothingI = buildToEdit.clothing!.indexOf("UNKNOWN") + if (clothingI !== -1) { + const copy = buildToEdit.clothing!.slice() + copy[clothingI] = ability as ClothingOnlyAbility | StackableAbility + handleChange({ + clothing: copy, + }) + return + } + + const shoesI = buildToEdit.shoes!.indexOf("UNKNOWN") + if (shoesI !== -1) { + const copy = buildToEdit.shoes!.slice() + copy[shoesI] = ability as ShoesOnlyAbility | StackableAbility + handleChange({ + shoes: copy, + }) + } + } + } + + const handleClickBuildAbility = ( + slot: "HEAD" | "CLOTHING" | "SHOES", + index: number + ) => { + if (slot === "HEAD") { + const copy = buildToEdit.headgear!.slice() + copy[index] = "UNKNOWN" + handleChange({ + headgear: copy, + }) + } else if (slot === "CLOTHING") { + const copy = buildToEdit.clothing!.slice() + copy[index] = "UNKNOWN" + handleChange({ + clothing: copy, + }) + } else { + const copy = buildToEdit.shoes!.slice() + copy[index] = "UNKNOWN" + handleChange({ + shoes: copy, + }) + } + } + return ( + <> + + + + {showOther && ( + + )} + + {showOther && ( + + + + + )} + + + + handleAbilityButtonClick(ability)} + /> + + + ) +} + +export default EditableBuilds diff --git a/frontend-react/src/components/builds/ViewSlots.tsx b/frontend-react/src/components/builds/ViewSlots.tsx index e4eab3c5a..55cea0125 100644 --- a/frontend-react/src/components/builds/ViewSlots.tsx +++ b/frontend-react/src/components/builds/ViewSlots.tsx @@ -1,16 +1,20 @@ import React from "react" import { Build, Ability } from "../../types" import AbilityIcon from "./AbilityIcon" -import { Flex, Box } from "@chakra-ui/core" +import { Flex, Box, BoxProps } from "@chakra-ui/core" interface ViewSlotsProps { build: Partial onAbilityClick?: (gear: "HEAD" | "CLOTHING" | "SHOES", index: number) => void } -const ViewSlots: React.FC = ({ build, onAbilityClick }) => { +const ViewSlots: React.FC = ({ + build, + onAbilityClick, + ...props +}) => { return ( - <> + {( build.headgear ?? @@ -76,7 +80,7 @@ const ViewSlots: React.FC = ({ build, onAbilityClick }) => { ))} - + ) } diff --git a/frontend-react/src/components/elements/Button.tsx b/frontend-react/src/components/elements/Button.tsx index d72c05da8..76b5476e0 100644 --- a/frontend-react/src/components/elements/Button.tsx +++ b/frontend-react/src/components/elements/Button.tsx @@ -13,6 +13,7 @@ interface ButtonProps { size?: "xs" | "sm" | "lg" | "md" icon?: IconType width?: string + color?: string outlined?: boolean disabled?: boolean loading?: boolean @@ -26,6 +27,7 @@ const Button: React.FC = ({ disabled, loading, width, + color, outlined = false, ...props }) => { @@ -33,7 +35,7 @@ const Button: React.FC = ({ return ( number } function buildToAP(build: Partial) { @@ -119,7 +96,12 @@ export default function useAbilityEffects(build: Partial) { title, effect: `${parseFloat((1 / (mInkConsume * effect[0])).toFixed(2))}`, effectFromMax: effect[1], + effectFromMaxActual: (getEffect(highMidLow, 57)[0] / effect[0]) * 100, ability: "ISM" as Ability, + getEffect: (ap: number) => + parseFloat( + (1 / (mInkConsume * getEffect(highMidLow, ap)[0])).toFixed(2) + ), }) } @@ -131,7 +113,12 @@ export default function useAbilityEffects(build: Partial) { (1 / (mInkConsumeRepeat * effect[0])).toFixed(2) )}`, effectFromMax: effect[1], + effectFromMaxActual: (getEffect(highMidLow, 57)[0] / effect[0]) * 100, ability: "ISM" as Ability, + getEffect: (ap: number) => + parseFloat( + (1 / (mInkConsumeRepeat * getEffect(highMidLow, ap)[0])).toFixed(2) + ), }) } @@ -143,7 +130,15 @@ export default function useAbilityEffects(build: Partial) { (1 / (mFullChargeInkConsume * effect[0])).toFixed(2) )}`, effectFromMax: effect[1], + effectFromMaxActual: (getEffect(highMidLow, 57)[0] / effect[0]) * 100, ability: "ISM" as Ability, + getEffect: (ap: number) => + parseFloat( + ( + 1 / + (mFullChargeInkConsume * getEffect(highMidLow, ap)[0]) + ).toFixed(2) + ), }) } @@ -155,7 +150,14 @@ export default function useAbilityEffects(build: Partial) { (1 / (mMinChargeInkConsume * effect[0])).toFixed(2) )}`, effectFromMax: effect[1], + effectFromMaxActual: (getEffect(highMidLow, 57)[0] / effect[0]) * 100, ability: "ISM" as Ability, + getEffect: (ap: number) => + parseFloat( + (1 / (mMinChargeInkConsume * getEffect(highMidLow, ap)[0])).toFixed( + 2 + ) + ), }) } @@ -172,7 +174,15 @@ export default function useAbilityEffects(build: Partial) { (1 / (mInkConsumeSplashJump * effect[0])).toFixed(2) )}`, effectFromMax: effect[1], + effectFromMaxActual: (getEffect(highMidLow, 57)[0] / effect[0]) * 100, ability: "ISM" as Ability, + getEffect: (ap: number) => + parseFloat( + ( + 1 / + (mInkConsumeSplashJump * getEffect(highMidLow, ap)[0]) + ).toFixed(2) + ), }) } else if (mInkConsumeSplashJump && mInkConsumeSplashStand) { toReturn.push({ @@ -181,7 +191,15 @@ export default function useAbilityEffects(build: Partial) { (1 / (mInkConsumeSplashStand * effect[0])).toFixed(2) )}`, effectFromMax: effect[1], + effectFromMaxActual: (getEffect(highMidLow, 57)[0] / effect[0]) * 100, ability: "ISM" as Ability, + getEffect: (ap: number) => + parseFloat( + ( + 1 / + (mInkConsumeSplashStand * getEffect(highMidLow, ap)[0]) + ).toFixed(2) + ), }) toReturn.push({ @@ -190,31 +208,57 @@ export default function useAbilityEffects(build: Partial) { (1 / (mInkConsumeSplashJump * effect[0])).toFixed(2) )}`, effectFromMax: effect[1], + effectFromMaxActual: (getEffect(highMidLow, 57)[0] / effect[0]) * 100, ability: "ISM" as Ability, + getEffect: (ap: number) => + parseFloat( + ( + 1 / + (mInkConsumeSplashJump * getEffect(highMidLow, ap)[0]) + ).toFixed(2) + ), }) } const mSideStepInkConsume = buildWeaponData.mSideStepInkConsume if (mSideStepInkConsume) { toReturn.push({ - title: "Dodge rolls per ink tank", + title: "Dodge roll ink consumption", effect: `${parseFloat( (mSideStepInkConsume * effect[0] * 100).toFixed(2) )}% of ink tank`, effectFromMax: effect[1], + effectFromMaxActual: parseFloat( + (mSideStepInkConsume * effect[0] * 100).toFixed(2) + ), ability: "ISM" as Ability, + getEffect: (ap: number) => + parseFloat( + (mSideStepInkConsume * getEffect(highMidLow, ap)[0] * 100).toFixed( + 2 + ) + ), }) } const mInkConsumeUmbrella = buildWeaponData.mInkConsumeUmbrella if (mInkConsumeUmbrella) { toReturn.push({ - title: "Brella launch ink consumption", + title: "Brella shield launch ink consumption", effect: `${parseFloat( (mInkConsumeUmbrella * effect[0] * 100).toFixed(2) )}% of ink tank`, effectFromMax: effect[1], + effectFromMaxActual: parseFloat( + (mInkConsumeUmbrella * effect[0] * 100).toFixed(2) + ), ability: "ISM" as Ability, + getEffect: (ap: number) => + parseFloat( + (mInkConsumeUmbrella * getEffect(highMidLow, ap)[0] * 100).toFixed( + 2 + ) + ), }) } @@ -246,6 +290,9 @@ export default function useAbilityEffects(build: Partial) { (effect[0] * inkConsumption * 100).toFixed(2) )}% of ink tank`, effectFromMax: effect[1], + effectFromMaxActual: parseFloat( + (effect[0] * inkConsumption * 100).toFixed(2) + ), ability: "ISS" as Ability, }, ] @@ -279,6 +326,8 @@ export default function useAbilityEffects(build: Partial) { (Math.ceil(effectSquid[0]) / 60).toFixed(2) )} seconds)`, effectFromMax: effectSquid[1], + effectFromMaxActual: + (effectSquid[0] / getEffect(highMidLowSquid, 0)[0]) * 100, ability: "REC" as Ability, }, /*{