diff --git a/src/app/routes/Dashboard.tsx b/src/app/routes/Dashboard.tsx index 84fa453..86c54df 100644 --- a/src/app/routes/Dashboard.tsx +++ b/src/app/routes/Dashboard.tsx @@ -1,5 +1,5 @@ import React, { useEffect, useState, useMemo } from 'react'; -import { styled } from '@mui/system'; +import { Box, Container, styled } from '@mui/system'; import { useDispatch, useSelector } from 'react-redux'; import { AppDispatch, RootState } from '../../store'; import { generatePermutations } from '../../features/armor-optimization/generate-permutations'; @@ -20,6 +20,7 @@ import { DecodedLoadoutData, DestinyArmor, FilteredPermutation, + FragmentStatModifications, StatName, SubclassConfig, } from '../../types/d2l-types'; @@ -46,87 +47,67 @@ import { updateSelectedExoticClassCombo, updateSelectedExoticItemHash, } from '../../store/DashboardReducer'; +import StatModifications from '../../features/subclass/StatModifications'; +import { Grid, Paper } from '@mui/material'; import { ManifestArmorStatMod, ManifestExoticArmor } from '../../types/manifest-types'; import { SharedLoadoutDto } from '../../features/loadouts/types'; -const PageContainer = styled('div')({ +const PageContainer = styled(Box)(({ theme }) => ({ display: 'flex', flexDirection: 'column', height: '100vh', overflow: 'hidden', -}); +})); -const Container = styled('div')({ +const ContentContainer = styled(Box)(({ theme }) => ({ flex: 1, display: 'flex', flexDirection: 'column', - alignItems: 'center', - padding: '20px', - width: '100vw', - boxSizing: 'border-box', + width: '100%', overflowY: 'auto', backgroundImage: `url(${greyBackground})`, backgroundSize: 'cover', backgroundPosition: 'center', - marginTop: '110px', - '::-webkit-scrollbar': { - width: '10px', - }, - '::-webkit-scrollbar-track': { - background: 'none', - }, - '::-webkit-scrollbar-thumb': { - background: 'grey', - borderRadius: '0', - }, -}); - -const BottomPane = styled('div')({ - display: 'flex', - width: '100%', - padding: '10px', - boxSizing: 'border-box', - justifyContent: 'space-between', - flexWrap: 'wrap', -}); + padding: theme.spacing(3), + paddingTop: '120px', +})); -const LeftPane = styled('div')({ +const LeftRightColumn = styled(Grid)(({ theme }) => ({ + width: '40%', display: 'flex', flexDirection: 'column', alignItems: 'center', - width: '100%', - maxWidth: '600px', - padding: '10px', - boxSizing: 'border-box', - marginTop: '-80px', - margin: '0 auto', -}); - -const RightPane = styled('div')({ - display: 'flex', - flexDirection: 'column', - alignItems: 'center', - width: '100%', - maxWidth: '800px', - padding: '5px', - boxSizing: 'border-box', - margin: '0 auto', -}); - -const DiamondButtonWrapper = styled('div')({ - marginTop: '50px', - marginBottom: '80px', - marginRight: '40px', - alignSelf: 'flex-end', -}); + [theme.breakpoints.down('md')]: { + width: '100%', + }, +})); -const NumberBoxesWrapper = styled('div')({ - marginBottom: '20px', +const MiddleColumn = styled(Grid)(({ theme }) => ({ + width: '20%', + [theme.breakpoints.down('md')]: { + width: '100%', + }, +})); + +const HeaderWrapper = styled(Box)({ + position: 'fixed', + top: 0, + left: 0, + right: 0, + zIndex: 1100, }); -const NewComponentWrapper = styled('div')({ - marginBottom: '20px', -}); +const NumberBoxesContainer = styled(Box)(({ theme }) => ({ + marginTop: theme.spacing(4), + marginLeft: theme.spacing(25), + backgroundColor: 'rgba(0, 0, 0, 0.1)', + backdropFilter: 'blur(5px)', + borderRadius: 0, + padding: theme.spacing(2), + alignSelf: 'flex-start', + width: 'auto', + maxWidth: '100%', +})); export const Dashboard: React.FC = () => { const dispatch = useDispatch(); @@ -138,6 +119,9 @@ export const Dashboard: React.FC = () => { ); const selectedCharacter = useSelector((state: RootState) => state.profile.selectedCharacter); + const fragments = useSelector( + (state: RootState) => state.loadoutConfig.loadout.subclassConfig.fragments + ); const [generatingPermutations, setGeneratingPermutations] = useState(false); const [subclasses, setSubclasses] = useState< { [key: number]: SubclassConfig | undefined } | undefined @@ -151,6 +135,30 @@ export const Dashboard: React.FC = () => { const [showAbilitiesModification, setShowAbilitiesModification] = useState(false); const [sharedLoadoutDto, setSharedLoadoutDto] = useState(undefined); + const fragmentStatModifications = useMemo(() => { + return fragments.reduce( + (acc, fragment) => { + if (fragment.itemHash !== 0) { + acc.mobility += fragment.mobilityMod || 0; + acc.resilience += fragment.resilienceMod || 0; + acc.recovery += fragment.recoveryMod || 0; + acc.discipline += fragment.disciplineMod || 0; + acc.intellect += fragment.intellectMod || 0; + acc.strength += fragment.strengthMod || 0; + } + return acc; + }, + { + mobility: 0, + resilience: 0, + recovery: 0, + discipline: 0, + intellect: 0, + strength: 0, + } as FragmentStatModifications + ); + }, [fragments]); + useEffect(() => { const updateProfile = async () => { await updateManifest(); @@ -321,13 +329,19 @@ export const Dashboard: React.FC = () => { return generatePermutations( selectedCharacter.armor, selectedExotic, - selectedExoticClassCombo + selectedExoticClassCombo, + fragmentStatModifications ); - return generatePermutations(selectedCharacter.armor, selectedExotic); + return generatePermutations( + selectedCharacter.armor, + selectedExotic, + undefined, + fragmentStatModifications + ); } return null; - }, [selectedCharacter, selectedExotic, selectedExoticClassCombo]); + }, [selectedCharacter, selectedExotic, selectedExoticClassCombo, fragmentStatModifications]); const filteredPermutations = useMemo(() => { let filtered: FilteredPermutation[] | null = null; @@ -347,17 +361,21 @@ export const Dashboard: React.FC = () => { characterClass: sharedLoadoutDto.characterClass as CharacterClass, }; setGeneratingPermutations(true); - const sharedLoadoutPermutation = filterFromSharedLoadout(decodedLoadoutData, permutations); + const sharedLoadoutPermutation = filterFromSharedLoadout( + decodedLoadoutData, + permutations, + fragmentStatModifications + ); filtered = sharedLoadoutPermutation === null ? null : [sharedLoadoutPermutation]; } else if (permutations && selectedValues) { setGeneratingPermutations(true); - filtered = filterPermutations(permutations, selectedValues); + filtered = filterPermutations(permutations, selectedValues, fragmentStatModifications); } setGeneratingPermutations(false); return filtered; - }, [permutations, selectedValues, sharedLoadoutDto]); + }, [permutations, selectedValues, sharedLoadoutDto, fragmentStatModifications]); useEffect(() => { if (filteredPermutations && sharedLoadoutDto) { @@ -436,39 +454,39 @@ export const Dashboard: React.FC = () => { /> ) : sharedLoadoutDto === undefined && selectedCharacter && selectedSubclass ? ( <> - {selectedCharacter?.emblem?.secondarySpecial && ( + - )} - - - - - - - - - - + + + + + + + + + - - - -

Armour Combinations

+
+
+ + + + {generatingPermutations ? (

Loading...

) : filteredPermutations ? ( @@ -479,9 +497,9 @@ export const Dashboard: React.FC = () => { ) : (

Loading....

)} - - - +
+
+
) : (
loading...
diff --git a/src/components/WaveTest.json b/src/components/WaveTest.json deleted file mode 100644 index fef0201..0000000 --- a/src/components/WaveTest.json +++ /dev/null @@ -1 +0,0 @@ -{"v":"5.9.0","fr":60,"ip":0,"op":300,"w":2560,"h":1440,"nm":"Untitled-1","ddd":0,"assets":[{"id":"video_0","w":935,"h":936,"u":"images/","p":"vid_0.psd","e":0},{"id":"video_1","w":1013,"h":1014,"u":"images/","p":"vid_1.psd","e":0},{"id":"video_2","w":935,"h":936,"u":"images/","p":"vid_2.psd","e":0},{"id":"video_3","w":1013,"h":1014,"u":"images/","p":"vid_3.psd","e":0},{"id":"video_4","w":1118,"h":1118,"u":"images/","p":"vid_4.psd","e":0},{"id":"video_5","w":1206,"h":1206,"u":"images/","p":"vid_5.psd","e":0},{"id":"video_6","w":172,"h":171,"u":"images/","p":"vid_6.psd","e":0},{"id":"video_7","w":603,"h":604,"u":"images/","p":"vid_7.psd","e":0},{"id":"video_8","w":911,"h":789,"u":"images/","p":"vid_8.psd","e":0},{"id":"video_9","w":911,"h":789,"u":"images/","p":"vid_9.psd","e":0},{"id":"video_10","w":911,"h":789,"u":"images/","p":"vid_10.psd","e":0},{"id":"video_11","w":2560,"h":1440,"u":"images/","p":"vid_11.psd","e":0},{"id":"video_12","w":1840,"h":1840,"u":"images/","p":"vid_12.psd","e":0},{"id":"video_13","w":2560,"h":1440,"u":"images/","p":"vid_13.psd","e":0},{"id":"video_14","w":2560,"h":1440,"u":"images/","p":"vid_14.psd","e":0}],"layers":[{"ddd":0,"ind":1,"ty":9,"nm":"Ellipse 2 copy 2","refId":"video_0","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[1279.5,714,0],"ix":2,"l":2},"a":{"a":0,"k":[467.5,468,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"hasMask":true,"masksProperties":[{"inv":false,"mode":"n","pt":{"a":0,"k":{"i":[[-257.846,0],[0,-257.846],[257.846,0],[0,257.846]],"o":[[257.846,0],[0,257.846],[-257.846,0],[0,-257.846]],"v":[[467.5,1.253],[934.372,468.125],[467.5,934.997],[0.628,468.125]],"c":true},"ix":1},"o":{"a":0,"k":100,"ix":3},"x":{"a":0,"k":0,"ix":4},"nm":"Mask 1"}],"ip":0,"op":300,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":9,"nm":"Ellipse 1 copy 4","refId":"video_1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[1279.5,714,0],"ix":2,"l":2},"a":{"a":0,"k":[506.5,507,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"hasMask":true,"masksProperties":[{"inv":false,"mode":"n","pt":{"a":0,"k":{"i":[[-278.766,0],[0,-278.766],[278.766,0],[0,278.766]],"o":[[278.766,0],[0,278.766],[-278.766,0],[0,-278.766]],"v":[[506.5,2.374],[1011.251,507.125],[506.5,1011.876],[1.749,507.125]],"c":true},"ix":1},"o":{"a":0,"k":100,"ix":3},"x":{"a":0,"k":0,"ix":4},"nm":"Mask 1"}],"ip":0,"op":300,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":9,"nm":"Ellipse 2 copy","refId":"video_2","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[1279.5,714,0],"ix":2,"l":2},"a":{"a":0,"k":[467.5,468,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"hasMask":true,"masksProperties":[{"inv":false,"mode":"n","pt":{"a":0,"k":{"i":[[-257.846,0],[0,-257.846],[257.846,0],[0,257.846]],"o":[[257.846,0],[0,257.846],[-257.846,0],[0,-257.846]],"v":[[467.5,1.253],[934.372,468.125],[467.5,934.997],[0.628,468.125]],"c":true},"ix":1},"o":{"a":0,"k":100,"ix":3},"x":{"a":0,"k":0,"ix":4},"nm":"Mask 1"}],"ip":0,"op":300,"st":0,"bm":0},{"ddd":0,"ind":4,"ty":9,"nm":"Ellipse 1 copy 3","refId":"video_3","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[1279.5,714,0],"ix":2,"l":2},"a":{"a":0,"k":[506.5,507,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"hasMask":true,"masksProperties":[{"inv":false,"mode":"n","pt":{"a":0,"k":{"i":[[-278.766,0],[0,-278.766],[278.766,0],[0,278.766]],"o":[[278.766,0],[0,278.766],[-278.766,0],[0,-278.766]],"v":[[506.5,2.374],[1011.251,507.125],[506.5,1011.876],[1.749,507.125]],"c":true},"ix":1},"o":{"a":0,"k":100,"ix":3},"x":{"a":0,"k":0,"ix":4},"nm":"Mask 1"}],"ip":0,"op":300,"st":0,"bm":0},{"ddd":0,"ind":5,"ty":9,"nm":"Ellipse 2","refId":"video_4","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[1280,720,0],"ix":2,"l":2},"a":{"a":0,"k":[559,559,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"hasMask":true,"masksProperties":[{"inv":false,"mode":"n","pt":{"a":0,"k":{"i":[[-307.014,0],[0,-307.014],[307.014,0],[0,307.014]],"o":[[307.014,0],[0,307.014],[-307.014,0],[0,-307.014]],"v":[[559,3.103],[1114.897,559],[559,1114.898],[3.102,559]],"c":true},"ix":1},"o":{"a":0,"k":100,"ix":3},"x":{"a":0,"k":0,"ix":4},"nm":"Mask 1"}],"ip":0,"op":300,"st":0,"bm":0},{"ddd":0,"ind":6,"ty":9,"nm":"Ellipse 1","refId":"video_5","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[1280,720,0],"ix":2,"l":2},"a":{"a":0,"k":[603,603,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"hasMask":true,"masksProperties":[{"inv":false,"mode":"n","pt":{"a":0,"k":{"i":[[-331.923,0],[0,-331.923],[331.923,0],[0,331.923]],"o":[[331.923,0],[0,331.923],[-331.923,0],[0,-331.923]],"v":[[603,2],[1204,603],[603,1204],[2,603]],"c":true},"ix":1},"o":{"a":0,"k":100,"ix":3},"x":{"a":0,"k":0,"ix":4},"nm":"Mask 1"}],"ip":0,"op":300,"st":0,"bm":0},{"ddd":0,"ind":7,"ty":9,"nm":"Ellipse 1 copy","refId":"video_6","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[1280,1150.5,0],"ix":2,"l":2},"a":{"a":0,"k":[86,85.5,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"hasMask":true,"masksProperties":[{"inv":false,"mode":"n","pt":{"a":0,"k":{"i":[[-46.276,0],[0,-46.276],[46.276,0],[0,46.276]],"o":[[46.276,0],[0,46.276],[-46.276,0],[0,-46.276]],"v":[[86,1.709],[169.791,85.5],[86,169.291],[2.209,85.5]],"c":true},"ix":1},"o":{"a":0,"k":100,"ix":3},"x":{"a":0,"k":0,"ix":4},"nm":"Mask 1"}],"ip":0,"op":300,"st":0,"bm":0},{"ddd":0,"ind":8,"ty":9,"nm":"Ellipse 1 copy 2","refId":"video_7","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[1279.5,720,0],"ix":2,"l":2},"a":{"a":0,"k":[301.5,302,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"hasMask":true,"masksProperties":[{"inv":false,"mode":"n","pt":{"a":0,"k":{"i":[[-165.548,0],[0,-165.548],[165.548,0],[0,165.548]],"o":[[165.548,0],[0,165.548],[-165.548,0],[0,-165.548]],"v":[[301.5,2.248],[601.252,302],[301.5,601.752],[1.748,302]],"c":true},"ix":1},"o":{"a":0,"k":100,"ix":3},"x":{"a":0,"k":0,"ix":4},"nm":"Mask 1"}],"ip":0,"op":300,"st":0,"bm":0},{"ddd":0,"ind":9,"ty":9,"nm":"Triangle 1 copy 3","refId":"video_8","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[361.5,1381.5,0],"ix":2,"l":2},"a":{"a":0,"k":[455.5,394.5,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"hasMask":true,"masksProperties":[{"inv":false,"mode":"n","pt":{"a":0,"k":{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[455.5,784],[4.344,2.5],[906.657,2.5]],"c":true},"ix":1},"o":{"a":0,"k":100,"ix":3},"x":{"a":0,"k":0,"ix":4},"nm":"Mask 1"}],"ip":0,"op":300,"st":0,"bm":0},{"ddd":0,"ind":10,"ty":9,"nm":"Triangle 1 copy 2","refId":"video_9","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[2197.5,1381.5,0],"ix":2,"l":2},"a":{"a":0,"k":[455.5,394.5,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"hasMask":true,"masksProperties":[{"inv":false,"mode":"n","pt":{"a":0,"k":{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[455.5,784],[4.344,2.5],[906.657,2.5]],"c":true},"ix":1},"o":{"a":0,"k":100,"ix":3},"x":{"a":0,"k":0,"ix":4},"nm":"Mask 1"}],"ip":0,"op":300,"st":0,"bm":0},{"ddd":0,"ind":11,"ty":9,"nm":"Triangle 1 copy","refId":"video_10","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[1279.5,-181.5,0],"ix":2,"l":2},"a":{"a":0,"k":[455.5,394.5,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"hasMask":true,"masksProperties":[{"inv":false,"mode":"n","pt":{"a":0,"k":{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[455.5,784],[4.344,2.5],[906.657,2.5]],"c":true},"ix":1},"o":{"a":0,"k":100,"ix":3},"x":{"a":0,"k":0,"ix":4},"nm":"Mask 1"}],"ip":0,"op":300,"st":0,"bm":0},{"ddd":0,"ind":12,"ty":9,"nm":"Triangle 1","refId":"video_11","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[1280,720,0],"ix":2,"l":2},"a":{"a":0,"k":[1280,720,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"ip":0,"op":300,"st":0,"bm":0},{"ddd":0,"ind":13,"ty":9,"nm":"Ellipse 1 copy 5","refId":"video_12","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[1749,714,0],"ix":2,"l":2},"a":{"a":0,"k":[920,920,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"hasMask":true,"masksProperties":[{"inv":false,"mode":"n","pt":{"a":0,"k":{"i":[[-506.975,0],[0,-506.975],[506.975,0],[0,506.975]],"o":[[506.975,0],[0,506.975],[-506.975,0],[0,-506.975]],"v":[[920.062,2.166],[1838.021,920.125],[920.062,1838.084],[2.104,920.125]],"c":true},"ix":1},"o":{"a":0,"k":100,"ix":3},"x":{"a":0,"k":0,"ix":4},"nm":"Mask 1"}],"ip":0,"op":300,"st":0,"bm":0},{"ddd":0,"ind":14,"ty":9,"nm":"Color Fill 1 copy","refId":"video_13","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[1280,720,0],"ix":2,"l":2},"a":{"a":0,"k":[1280,720,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"sy":[{"bm":{"a":0,"k":1,"ix":1},"o":{"a":0,"k":100,"ix":2},"gf":{"a":0,"ix":3},"gs":{"a":0,"k":100,"ix":4},"a":{"a":0,"k":0,"ix":5},"gt":{"a":0,"k":4,"ix":6},"re":{"a":0,"k":0,"ix":7},"al":{"a":0,"k":1,"ix":8},"s":{"a":0,"k":75,"ix":9},"of":{"a":0,"k":[0,0],"ix":10},"ty":8,"nm":"Gradient Overlay"}],"ip":0,"op":300,"st":0,"bm":0},{"ddd":0,"ind":15,"ty":9,"nm":"Color Fill 1","refId":"video_14","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[1280,720,0],"ix":2,"l":2},"a":{"a":0,"k":[1280,720,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"sy":[{"bm":{"a":0,"k":1,"ix":1},"o":{"a":0,"k":100,"ix":2},"gf":{"a":0,"ix":3},"gs":{"a":0,"k":100,"ix":4},"a":{"a":0,"k":0,"ix":5},"gt":{"a":0,"k":4,"ix":6},"re":{"a":0,"k":0,"ix":7},"al":{"a":0,"k":1,"ix":8},"s":{"a":0,"k":75,"ix":9},"of":{"a":0,"k":[0,0],"ix":10},"ty":8,"nm":"Gradient Overlay"}],"ip":0,"op":300,"st":0,"bm":0}],"markers":[]} \ No newline at end of file diff --git a/src/features/armor-optimization/NumberBoxes.tsx b/src/features/armor-optimization/NumberBoxes.tsx index c6dba47..62dc5c6 100644 --- a/src/features/armor-optimization/NumberBoxes.tsx +++ b/src/features/armor-optimization/NumberBoxes.tsx @@ -1,20 +1,11 @@ import React from 'react'; import { styled } from '@mui/material/styles'; -import { Box, Paper, Grid, ButtonBase } from '@mui/material'; +import { Box, Grid, ButtonBase } from '@mui/material'; import { STATS } from '../../lib/bungie_api/constants'; import { updateSelectedValues } from '../../store/DashboardReducer'; import { useDispatch, useSelector } from 'react-redux'; import { RootState } from '../../store'; -const ContainerWithBorder = styled(Paper)(({ theme }) => ({ - border: `1px solid ${theme.palette.divider}`, - padding: theme.spacing(2), - width: 'fit-content', - margin: theme.spacing(1, 0), - backgroundColor: 'rgba(0, 0, 0, 0.5)', - backdropFilter: 'blur(10px)', -})); - const StatRow = styled(Grid)(({ theme }) => ({ marginBottom: theme.spacing(1), })); @@ -22,6 +13,7 @@ const StatRow = styled(Grid)(({ theme }) => ({ const NumberBoxContainer = styled(Box)({ display: 'flex', alignItems: 'center', + justifyContent: 'flex-start', }); interface NumberBoxProps { @@ -31,16 +23,16 @@ interface NumberBoxProps { const NumberBox = styled(ButtonBase, { shouldForwardProp: (prop) => prop !== 'isSelected', })(({ isSelected }) => ({ - width: 40, - height: 40, - lineHeight: '40px', + width: 32, // Increased size + height: 32, // Increased size + lineHeight: '32px', textAlign: 'center', - border: `1px solid ${isSelected ? '#bdab6d' : 'white'}`, - marginRight: 2, - backgroundColor: 'transparent', + border: `1px solid ${isSelected ? '#bdab6d' : 'rgba(255, 255, 255, 0.5)'}`, + margin: '0 2px', // Increased margin between boxes + backgroundColor: isSelected ? 'rgba(189, 171, 109, 0.2)' : 'transparent', color: isSelected ? '#bdab6d' : 'white', cursor: 'pointer', - fontSize: 16, + fontSize: 14, // Increased font size '&:hover': { backgroundColor: 'rgba(255, 255, 255, 0.1)', }, @@ -49,7 +41,7 @@ const NumberBox = styled(ButtonBase, { const StatIcon = styled('img')({ width: 24, height: 24, - marginRight: 10, + marginRight: 12, }); const statIcons: Record = { @@ -74,32 +66,28 @@ const NumberBoxes: React.FC = () => { }; return ( - - - {STATS.map((stat) => ( - - - - - - - {[10, 20, 30, 40, 50, 60, 70, 80, 90, 100].map((number) => ( - handleSelect(stat, number)} - > - {number} - - ))} - - - - ))} - - + + {STATS.map((stat) => ( + + + + + + + {[10, 20, 30, 40, 50, 60, 70, 80, 90, 100].map((number) => ( + handleSelect(stat, number)} + > + {number / 10} + + ))} + + + + ))} + ); }; diff --git a/src/features/armor-optimization/StatsTable.tsx b/src/features/armor-optimization/StatsTable.tsx index d44e5c1..44eba91 100644 --- a/src/features/armor-optimization/StatsTable.tsx +++ b/src/features/armor-optimization/StatsTable.tsx @@ -3,11 +3,12 @@ import { styled } from '@mui/material/styles'; import { Box, Card, Grid, Typography, IconButton, Tooltip } from '@mui/material'; import ChevronLeftIcon from '@mui/icons-material/ChevronLeft'; import ChevronRightIcon from '@mui/icons-material/ChevronRight'; -import { FilteredPermutation, DestinyArmor } from '../../types/d2l-types'; -import { useDispatch } from 'react-redux'; +import { FilteredPermutation, DestinyArmor, StatName } from '../../types/d2l-types'; +import { useSelector } from 'react-redux'; import ArmorIcon from '../../components/ArmorIcon'; import { STAT_MOD_HASHES, STATS } from '../../lib/bungie_api/constants'; import { db } from '../../store/db'; +import { RootState } from '../../store'; interface StatsTableProps { permutations: FilteredPermutation[]; @@ -22,7 +23,7 @@ const StatsTableContainer = styled(Box)(({ theme }) => ({ justifyContent: 'center', position: 'relative', marginTop: theme.spacing(-1), - gap: theme.spacing(1), // Add gap between children + gap: theme.spacing(1), })); const CardContainer = styled(Box)(({ theme }) => ({ @@ -32,7 +33,7 @@ const CardContainer = styled(Box)(({ theme }) => ({ justifyContent: 'center', height: '100%', width: '440px', - padding: theme.spacing(0, 1), // Add horizontal padding + padding: theme.spacing(0, 1), })); const StyledCard = styled(Card)(({ theme }) => ({ @@ -66,9 +67,11 @@ const ModsRow = styled(Grid)(({ theme }) => ({ justifyContent: 'center', width: '100%', })); + const StatValue = styled(Typography)({ color: 'white', }); + const StatContainer = styled(Box)(({ theme }) => ({ display: 'flex', flexDirection: 'column', @@ -114,19 +117,43 @@ const StatsTable: React.FC = ({ permutations, onPermutationClic const [currentPage, setCurrentPage] = useState(0); const itemsPerPage = 4; + const subclassConfig = useSelector( + (state: RootState) => state.loadoutConfig.loadout.subclassConfig + ); + + const fragmentStatModifications = useMemo(() => { + const modifications: { [key in StatName]: number } = { + mobility: 0, + resilience: 0, + recovery: 0, + discipline: 0, + intellect: 0, + strength: 0, + }; + + subclassConfig.fragments.forEach((fragment) => { + if (fragment.mobilityMod) modifications.mobility += fragment.mobilityMod; + if (fragment.resilienceMod) modifications.resilience += fragment.resilienceMod; + if (fragment.recoveryMod) modifications.recovery += fragment.recoveryMod; + if (fragment.disciplineMod) modifications.discipline += fragment.disciplineMod; + if (fragment.intellectMod) modifications.intellect += fragment.intellectMod; + if (fragment.strengthMod) modifications.strength += fragment.strengthMod; + }); + + return modifications; + }, [subclassConfig.fragments]); + const paginatedData = useMemo(() => { const start = currentPage * itemsPerPage; const end = start + itemsPerPage; return permutations.slice(start, end); }, [currentPage, permutations]); - const calculateTotal = ( - perm: FilteredPermutation, - stat: keyof FilteredPermutation['modsArray'] - ) => { + const calculateTotal = (perm: FilteredPermutation, stat: StatName) => { const baseSum = perm.permutation.reduce((sum, item) => sum + (item[stat] || 0), 0); const modSum = perm.modsArray[stat]?.reduce((sum, mod) => sum + mod, 0) || 0; - return baseSum + modSum; + const fragmentMod = fragmentStatModifications[stat] || 0; + return baseSum + modSum + fragmentMod; }; const [modIcons, setModIcons] = useState>({}); @@ -148,13 +175,14 @@ const StatsTable: React.FC = ({ permutations, onPermutationClic fetchModIcons(); }, []); - const statIcons: Record = { - mobility: '/assets/mob.png', - resilience: '/assets/res.png', - recovery: '/assets/rec.png', - discipline: '/assets/disc.png', - intellect: '/assets/int.png', - strength: '/assets/str.png', + + const statIcons: Record = { + mobility: 'src/assets/mob.png', + resilience: 'src/assets/res.png', + recovery: 'src/assets/rec.png', + discipline: 'src/assets/disc.png', + intellect: 'src/assets/int.png', + strength: 'src/assets/str.png', }; const formatArmorStats = (armor: DestinyArmor) => { @@ -192,19 +220,17 @@ const StatsTable: React.FC = ({ permutations, onPermutationClic ))} - {(STATS as (keyof FilteredPermutation['modsArray'])[]).map((stat) => ( + {(STATS as StatName[]).map((stat) => ( - - {calculateTotal(perm, stat as keyof FilteredPermutation['modsArray'])} - + {calculateTotal(perm, stat)} ))} - {(STATS as (keyof FilteredPermutation['modsArray'])[]).map((stat) => + {(STATS as StatName[]).map((stat) => perm.modsArray[stat].map((mod, idx) => ( { - let acceptedCount = 0; - let discardedCount = 0; - const results: FilteredPermutation[] = []; for (const permutation of permutations) { @@ -31,22 +30,21 @@ export const filterPermutations = ( const artificeCount = permutation.filter((armor) => armor.artifice).length; - // Calculate initial stat totals and deficits const statDeficits: Record = {}; for (const stat in thresholds) { - const key = stat.toLowerCase() as keyof DestinyArmor; - const totalStat = permutation.reduce((sum, item) => sum + ((item[key] as number) || 0), 0); + const key = stat.toLowerCase() as keyof DestinyArmor & keyof FragmentStatModifications; + const totalStat = permutation.reduce((sum, item) => sum + ((item[key] as number) || 0), 0) + + fragmentStatModifications[key]; statDeficits[stat] = Math.max(0, thresholds[stat] - totalStat); } - // Try to find a valid mod combination const tryModCombination = ( statIndex: number, artificeUsed: number, regularModsUsed: number ): boolean => { if (statIndex === Object.keys(statDeficits).length) { - return true; // All stats processed successfully + return true; } const stat = Object.keys(statDeficits)[statIndex]; @@ -57,9 +55,8 @@ export const filterPermutations = ( } const combinations = precalculatedModCombinations[deficit] || []; - for (const [artifice, minor, major, total] of combinations) { + for (const [artifice, minor, major] of combinations) { if (artifice <= artificeCount - artificeUsed && minor + major <= 5 - regularModsUsed) { - // Try this combination modsArray[stat.toLowerCase() as keyof FilteredPermutation['modsArray']] = [ ...Array(artifice).fill(3), ...Array(minor).fill(5), @@ -76,7 +73,6 @@ export const filterPermutations = ( return true; } - // If it doesn't work, reset this stat's mods modsArray[stat.toLowerCase() as keyof FilteredPermutation['modsArray']] = []; } } @@ -85,10 +81,7 @@ export const filterPermutations = ( }; if (tryModCombination(0, 0, 0)) { - acceptedCount++; results.push({ permutation, modsArray }); - } else { - discardedCount++; } } @@ -97,11 +90,9 @@ export const filterPermutations = ( export function filterFromSharedLoadout( decodedLoadout: DecodedLoadoutData, - permutations: DestinyArmor[][] + permutations: DestinyArmor[][], + fragmentStatModifications: FragmentStatModifications ): FilteredPermutation | null { - console.log('Starting findMatchingArmorSet with decoded loadout:', decodedLoadout); - - // Start with only the highest priority stat let currentThresholds: { [K in StatName]: number } = { mobility: 0, resilience: 0, @@ -115,41 +106,28 @@ export function filterFromSharedLoadout( const currentStat = decodedLoadout.statPriority[priorityIndex]; currentThresholds[currentStat] = 100; - console.log(`\nConsidering stat: ${currentStat}`); - console.log('Current thresholds:', currentThresholds); - let found = false; while (!found) { - console.log('Filtering permutations...'); - const filteredPermutations = filterPermutations(permutations, currentThresholds); - console.log(`Found ${filteredPermutations.length} matching permutations`); + const filteredPermutations = filterPermutations(permutations, currentThresholds, fragmentStatModifications); if (filteredPermutations.length > 0) { found = true; if (priorityIndex === decodedLoadout.statPriority.length - 1) { - // If we're on the last stat and found a match, we're done - console.log('Match found! Returning best matching permutation.'); - console.log('Matched Permutation Details:'); - console.log('Mods Array:', filteredPermutations[0].modsArray); return filteredPermutations[0]; } } else { - // Reduce the threshold of the current stat currentThresholds[currentStat] = Math.max(0, currentThresholds[currentStat] - 10); - console.log(`Reduced ${currentStat} threshold to ${currentThresholds[currentStat]}`); if (currentThresholds[currentStat] === 0) { - // If we've reduced the current stat to 0 and still no match, move to the next stat break; } } } if (!found) { - console.log(`Could not find a match considering up to ${currentStat}`); break; } } return null; -} +} \ No newline at end of file diff --git a/src/features/armor-optimization/generate-permutations.ts b/src/features/armor-optimization/generate-permutations.ts index 5eb4349..1029299 100644 --- a/src/features/armor-optimization/generate-permutations.ts +++ b/src/features/armor-optimization/generate-permutations.ts @@ -1,15 +1,29 @@ import { ARMOR } from '../../lib/bungie_api/constants'; -import { DestinyArmor, ArmorBySlot, armor, ExoticClassCombo } from '../../types/d2l-types'; import MaxHeap from 'heap-js'; - -export const generatePermutations = ( +import { + armor, + DestinyArmor, + ArmorBySlot, + ExoticClassCombo, + FragmentStatModifications +} from '../../types/d2l-types'; + +export function generatePermutations( armorClass: ArmorBySlot, selectedExoticItem: { itemHash: number | null; slot: armor | null } = { itemHash: null, slot: null, }, - selectedExoticClassCombo?: ExoticClassCombo -): DestinyArmor[][] => { + selectedExoticClassCombo?: ExoticClassCombo, + fragmentStatModifications: FragmentStatModifications = { + mobility: 0, + resilience: 0, + recovery: 0, + discipline: 0, + intellect: 0, + strength: 0 + } +): DestinyArmor[][] { const { helmet, arms, legs, chest, classItem } = armorClass; let filteredHelmet = helmet; @@ -18,54 +32,55 @@ export const generatePermutations = ( let filteredChest = chest; let filteredClass = classItem; - switch (selectedExoticItem.slot) { - case ARMOR.HELMET: - filteredHelmet = helmet.filter( - (item) => Number(item.itemHash) === selectedExoticItem.itemHash - ); - filteredArms = arms.filter((item) => !item.exotic); - filteredLegs = legs.filter((item) => !item.exotic); - filteredChest = chest.filter((item) => !item.exotic); - filteredClass = classItem.filter((item) => !item.exotic); - break; - case ARMOR.GAUNTLETS: - filteredHelmet = helmet.filter((item) => !item.exotic); - filteredArms = arms.filter((item) => Number(item.itemHash) === selectedExoticItem.itemHash); - filteredLegs = legs.filter((item) => !item.exotic); - filteredChest = chest.filter((item) => !item.exotic); - filteredClass = classItem.filter((item) => !item.exotic); - break; - case ARMOR.LEG_ARMOR: - filteredHelmet = helmet.filter((item) => !item.exotic); - filteredArms = arms.filter((item) => !item.exotic); - filteredLegs = legs.filter((item) => Number(item.itemHash) === selectedExoticItem.itemHash); - filteredChest = chest.filter((item) => !item.exotic); - filteredClass = classItem.filter((item) => !item.exotic); - break; - case ARMOR.CHEST_ARMOR: - filteredHelmet = helmet.filter((item) => !item.exotic); - filteredArms = arms.filter((item) => !item.exotic); - filteredLegs = legs.filter((item) => !item.exotic); - filteredChest = chest.filter((item) => Number(item.itemHash) === selectedExoticItem.itemHash); - filteredClass = classItem.filter((item) => !item.exotic); - break; - case ARMOR.CLASS_ARMOR: - filteredHelmet = helmet.filter((item) => !item.exotic); - filteredArms = arms.filter((item) => !item.exotic); - filteredLegs = legs.filter((item) => !item.exotic); - filteredChest = chest.filter((item) => !item.exotic); - filteredClass = selectedExoticClassCombo - ? classItem.filter((item) => - selectedExoticClassCombo.instanceHashes.includes(item.instanceHash) - ) - : classItem.filter((item) => Number(item.itemHash) === selectedExoticItem.itemHash); - break; + if (selectedExoticItem.slot !== null) { + switch (selectedExoticItem.slot) { + case ARMOR.HELMET: + filteredHelmet = helmet.filter( + (item) => Number(item.itemHash) === selectedExoticItem.itemHash + ); + filteredArms = arms.filter((item) => !item.exotic); + filteredLegs = legs.filter((item) => !item.exotic); + filteredChest = chest.filter((item) => !item.exotic); + filteredClass = classItem.filter((item) => !item.exotic); + break; + case ARMOR.GAUNTLETS: + filteredHelmet = helmet.filter((item) => !item.exotic); + filteredArms = arms.filter((item) => Number(item.itemHash) === selectedExoticItem.itemHash); + filteredLegs = legs.filter((item) => !item.exotic); + filteredChest = chest.filter((item) => !item.exotic); + filteredClass = classItem.filter((item) => !item.exotic); + break; + case ARMOR.LEG_ARMOR: + filteredHelmet = helmet.filter((item) => !item.exotic); + filteredArms = arms.filter((item) => !item.exotic); + filteredLegs = legs.filter((item) => Number(item.itemHash) === selectedExoticItem.itemHash); + filteredChest = chest.filter((item) => !item.exotic); + filteredClass = classItem.filter((item) => !item.exotic); + break; + case ARMOR.CHEST_ARMOR: + filteredHelmet = helmet.filter((item) => !item.exotic); + filteredArms = arms.filter((item) => !item.exotic); + filteredLegs = legs.filter((item) => !item.exotic); + filteredChest = chest.filter((item) => Number(item.itemHash) === selectedExoticItem.itemHash); + filteredClass = classItem.filter((item) => !item.exotic); + break; + case ARMOR.CLASS_ARMOR: + filteredHelmet = helmet.filter((item) => !item.exotic); + filteredArms = arms.filter((item) => !item.exotic); + filteredLegs = legs.filter((item) => !item.exotic); + filteredChest = chest.filter((item) => !item.exotic); + filteredClass = selectedExoticClassCombo + ? classItem.filter((item) => + selectedExoticClassCombo.instanceHashes.includes(item.instanceHash) + ) + : classItem.filter((item) => Number(item.itemHash) === selectedExoticItem.itemHash); + break; + } } const armorTypes = [filteredHelmet, filteredArms, filteredLegs, filteredChest]; - // find best class armor to use for permutation - // if additional class armor filtering is implemented, do so here + // Find best class armor to use for permutation let masterworkedClassArmor = undefined; let artificeMasterworkedClassArmor = undefined; @@ -87,20 +102,21 @@ export const generatePermutations = ( : artificeMasterworkedClassArmor; const heap = new MaxHeap((a: DestinyArmor[], b: DestinyArmor[]) => { - const sumStats = (items: DestinyArmor[]) => - items.reduce( - (sum: number, item: DestinyArmor) => - sum + - item.mobility + - item.resilience + - item.recovery + - item.discipline + - item.intellect + - item.strength, - 0 + const getTotalStats = (permutation: DestinyArmor[]) => { + const totalStats = permutation.reduce( + (sum, item) => ({ + mobility: sum.mobility + item.mobility, + resilience: sum.resilience + item.resilience, + recovery: sum.recovery + item.recovery, + discipline: sum.discipline + item.discipline, + intellect: sum.intellect + item.intellect, + strength: sum.strength + item.strength + }), + { ...fragmentStatModifications } // Start with fragment modifications ); - - return sumStats(a) - sumStats(b); + return Object.values(totalStats).reduce((a, b) => a + b, 0); + }; + return getTotalStats(b) - getTotalStats(a); }); const generate = ( @@ -111,38 +127,36 @@ export const generatePermutations = ( if (currentTypeIndex === armorTypes.length) { const modifiedPermutation = [...currentPermutation, bestClassArmor]; const totalStats = modifiedPermutation.reduce( - (sum: number, item: DestinyArmor) => - sum + - item.mobility + - item.resilience + - item.recovery + - item.discipline + - item.intellect + - item.strength, - 0 + (sum, item) => ({ + mobility: sum.mobility + item.mobility, + resilience: sum.resilience + item.resilience, + recovery: sum.recovery + item.recovery, + discipline: sum.discipline + item.discipline, + intellect: sum.intellect + item.intellect, + strength: sum.strength + item.strength + }), + { ...fragmentStatModifications } // Start with fragment modifications ); + const totalSum = Object.values(totalStats).reduce((a, b) => a + b, 0); + + const baseStats = modifiedPermutation.reduce((sum, item) => + sum + item.mobility + item.resilience + item.recovery + + item.discipline + item.intellect + item.strength, 0); + if (heap.size() < 30000) { heap.push(modifiedPermutation); } else { const smallest = heap.peek(); - if ( - smallest && - totalStats > - smallest.reduce( - (sum: number, item: DestinyArmor) => - sum + - item.mobility + - item.resilience + - item.recovery + - item.discipline + - item.intellect + - item.strength, - 0 - ) - ) { - heap.pop(); - heap.push(modifiedPermutation); + if (smallest) { + const smallestTotalSum = smallest.reduce((sum, item) => + sum + item.mobility + item.resilience + item.recovery + + item.discipline + item.intellect + item.strength, 0) + + Object.values(fragmentStatModifications).reduce((a, b) => a + b, 0); + if (totalSum > smallestTotalSum) { + heap.pop(); + heap.push(modifiedPermutation); + } } } return; @@ -164,4 +178,4 @@ export const generatePermutations = ( generate([], 0, 0); return heap.toArray(); -}; +} \ No newline at end of file diff --git a/src/features/subclass/StatModifications.tsx b/src/features/subclass/StatModifications.tsx new file mode 100644 index 0000000..e21fd35 --- /dev/null +++ b/src/features/subclass/StatModifications.tsx @@ -0,0 +1,88 @@ +import React from 'react'; +import { Box, styled } from '@mui/material'; +import { useSelector } from 'react-redux'; +import { createSelector } from '@reduxjs/toolkit'; +import { RootState } from '../../store'; +import { ManifestStatPlug } from '../../types/manifest-types'; + +const StatModificationsContainer = styled(Box)(({ theme }) => ({ + padding: theme.spacing(2), + backgroundColor: 'rgba(0, 0, 0, 0.2)', + backdropFilter: 'blur(5px)', + borderRadius: 0, + border: '1px solid rgba(255, 255, 255, 0.3)', +})); + +const StatModificationItem = styled('div')(({ theme }) => ({ + display: 'flex', + alignItems: 'center', + fontFamily: 'Helvetica, Arial, sans-serif', + fontWeight: 'bold', + fontSize: '1rem', + marginBottom: theme.spacing(0.5), + textShadow: '1px 1px 2px rgba(0, 0, 0, 0.8)', +})); + +const StatIcon = styled('img')({ + width: '20px', + height: '20px', + marginRight: '8px', +}); + +const statIcons: Record = { + mobility: 'src/assets/mob.png', + resilience: 'src/assets/res.png', + recovery: 'src/assets/rec.png', + discipline: 'src/assets/disc.png', + intellect: 'src/assets/int.png', + strength: 'src/assets/str.png', +}; + +const selectFragmentStatModifications = createSelector( + (state: RootState) => state.loadoutConfig.loadout.subclassConfig.fragments, + (fragments: ManifestStatPlug[]) => + fragments.reduce((acc, fragment) => { + if (fragment.itemHash !== 0) { + const stats = ['mobility', 'resilience', 'recovery', 'discipline', 'intellect', 'strength']; + stats.forEach((stat) => { + const value = fragment[`${stat}Mod` as keyof ManifestStatPlug] as number; + if (value !== 0) { + acc.push({ stat, value, name: fragment.name }); + } + }); + } + return acc; + }, [] as { stat: string; value: number; name: string }[]) +); + +const StatModifications: React.FC = () => { + const modifications = useSelector(selectFragmentStatModifications); + + if (modifications.length === 0) { + return null; + } + + return ( + + {modifications.map(({ stat, value, name }, index) => { + const color = value > 0 ? 'green' : 'red'; + const sign = value > 0 ? '+' : ''; + return ( + + + + {sign} + {value} {name} + + + ); + })} + + ); +}; + +export default StatModifications; diff --git a/src/features/subclass/SubclassCustomizationWrapper.tsx b/src/features/subclass/SubclassCustomizationWrapper.tsx index 47d7650..717857d 100644 --- a/src/features/subclass/SubclassCustomizationWrapper.tsx +++ b/src/features/subclass/SubclassCustomizationWrapper.tsx @@ -1,9 +1,9 @@ import React from 'react'; import AbilitiesModification from './AbilitiesModification'; +import StatModifications from './StatModifications'; import './SubclassCustomizationWrapper.css'; import { Button, Box } from '@mui/material'; import { styled } from '@mui/material/styles'; -import { ManifestSubclass } from '../../types/manifest-types'; import { SubclassConfig } from '../../types/d2l-types'; interface SubclassCustomizationWrapperProps { @@ -42,6 +42,7 @@ const SubclassCustomizationWrapper: React.FC Back + diff --git a/src/types/d2l-types.ts b/src/types/d2l-types.ts index e5fbdde..73e0670 100644 --- a/src/types/d2l-types.ts +++ b/src/types/d2l-types.ts @@ -159,3 +159,12 @@ export interface FilteredPermutation { strength: number[]; }; } + +export interface FragmentStatModifications { + mobility: number; + resilience: number; + recovery: number; + discipline: number; + intellect: number; + strength: number; +} \ No newline at end of file