Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Save loadout in game menu #61

Merged
merged 6 commits into from
Sep 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/components/LoadoutCustomization.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import EquipLoadout from '../features/loadouts/components/EquipLoadout';
import AbilitiesModification from '../features/subclass/AbilitiesModification';
import ShareLoadout from '../features/loadouts/components/ShareLoadout';
import { SubclassConfig } from '../types/d2l-types';
import SaveLoadout from '../features/loadouts/components/SaveLoadout';

interface LoadoutCustomizationProps {
onBackClick: () => void;
Expand Down
1 change: 0 additions & 1 deletion src/features/armor-mods/components/RequiredMod.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { useState } from 'react';
import { ManifestArmorStatMod } from '../../../types/manifest-types';
import { Tooltip, styled } from '@mui/material';
import { autoEquipStatMod } from '../mod-utils';
Expand Down
3 changes: 2 additions & 1 deletion src/features/loadouts/components/EquipLoadout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import LoadingBorder from './LoadingBorder';
import FadeIn from './FadeIn';
import { equipLoadout } from '../util/loadout-utils';
import { TransitionProps } from '@mui/material/transitions';
import SaveLoadout from './SaveLoadout';

const StyledTitle = styled(Typography)(({ theme }) => ({
paddingBottom: theme.spacing(1),
Expand Down Expand Up @@ -268,7 +269,7 @@ const EquipLoadout: React.FC = () => {
</Grid>
<Grid item md={4}>
<FadeIn delay={600}>
<Button>Save in-game</Button>
<SaveLoadout />
</FadeIn>
</Grid>
</>
Expand Down
179 changes: 179 additions & 0 deletions src/features/loadouts/components/SaveLoadout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
import {
Button,
Drawer,
Grid,
Box,
Autocomplete,
TextField,
Select,
FormControl,
InputLabel,
ImageList,
styled,
} from '@mui/material';
import { useState } from 'react';
import useLoadoutIdentifiers from '../hooks/use-loadout-identifiers';
import {
ManifestLoadoutColor,
ManifestLoadoutIcon,
ManifestLoadoutName,
} from '../../../types/manifest-types';
import useSelectedCharacterLoadouts from '../hooks/use-selected-character-loadouts';
import { snapShotLoadoutRequest } from '../../../lib/bungie_api/requests';
import { store } from '../../../store';

const LoadoutSlot = styled('img')(({ theme }) => ({
backgroundSize: 'cover',
backgroundPosition: 'center',
width: '51%',
height: 'auto',
border: '2px outset transparent',
'&:hover': { border: '2px solid blue' },
}));

export default function SaveLoadout() {
const [loadoutDrawerOpen, setLoadoutDrawerOpen] = useState<boolean>(false);
const [loadoutName, setLoadoutName] = useState<ManifestLoadoutName | null>(null);
const [loadoutColor, setLoadoutColor] = useState<ManifestLoadoutColor | null>(null);
const [loadoutIcon, setLoadoutIcon] = useState<ManifestLoadoutIcon | null>(null);
const [identifiersSet, setIdentifiersSet] = useState<boolean>(false);

const loadoutIdentifiers = useLoadoutIdentifiers();
const loadouts = useSelectedCharacterLoadouts();

function handleBackClick() {
setLoadoutDrawerOpen(false);
setLoadoutName(null);
setLoadoutColor(null);
setLoadoutIcon(null);
}

const SetIdentifiersDrawer = (
<Grid container alignItems="center" textAlign="center" rowGap={3} paddingX={4} paddingY={3}>
<Grid item md={12}>
SET IDENTIFIERS
</Grid>
<Grid item md={12}>
<Autocomplete
disablePortal
id="loadout-names"
value={loadoutName}
onChange={(event, newValue) => setLoadoutName(newValue)}
options={loadoutIdentifiers.loadoutNames}
getOptionLabel={(option) => option.name}
renderInput={(params) => <TextField {...params} label="NAME" />}
/>
</Grid>
<Grid item md={12}>
<FormControl fullWidth>
<InputLabel id="loadout-colors-label">COLOR</InputLabel>
<Select
labelId="loadout-colors-label"
id="loadout-colors"
label="COLOR"
value={loadoutColor}
renderValue={(selected) => <img src={selected?.imagePath} width="20%" height="auto" />}
>
<ImageList cols={4}>
{loadoutIdentifiers.loadoutColors.map((color) => (
<img
src={color.imagePath}
width="60%"
height="auto"
onClick={() => setLoadoutColor(color)}
/>
))}
</ImageList>
</Select>
</FormControl>
</Grid>
<Grid item md={12}>
<FormControl fullWidth>
<InputLabel id="loadout-icons-label">ICON</InputLabel>
<Select
labelId="loadout-icons-label"
id="loadout-icons"
label="ICON"
value={loadoutIcon}
renderValue={(selected) => <img src={selected?.imagePath} width="20%" height="auto" />}
>
<ImageList cols={4}>
{loadoutIdentifiers.loadoutIcons.map((icon) => (
<img
src={icon.imagePath}
width="60%"
height="auto"
onClick={() => setLoadoutIcon(icon)}
/>
))}
</ImageList>
</Select>
</FormControl>
</Grid>
<Grid item md={6}>
<Button onClick={handleBackClick}>BACK</Button>
</Grid>
<Grid item md={6}>
<Button
disabled={loadoutName === null && loadoutColor === null && loadoutIcon === null}
onClick={() => setIdentifiersSet(true)}
>
NEXT
</Button>
</Grid>
</Grid>
);

const SelectLoadoutSlotDrawer = (
<Grid container alignItems="center" textAlign="center" rowGap={2} paddingX={4} paddingY={3}>
<Grid item md={12}>
SELECT SLOT TO OVERWRITE
</Grid>
{loadouts?.map((loadout, index) => (
<Grid item md={6}>
<LoadoutSlot
onClick={async () => {
const characterId = store.getState().profile.selectedCharacter?.id;

if (characterId && loadoutColor && loadoutIcon && loadoutName)
await snapShotLoadoutRequest(
String(characterId),
loadoutColor?.hash,
loadoutIcon?.hash,
index,
loadoutName?.hash
);

setIdentifiersSet(false);
setLoadoutDrawerOpen(false);
}}
src={
loadoutIdentifiers.loadoutIcons.find((icon) => icon.hash === loadout.iconHash)
?.imagePath
}
style={{
backgroundImage: `url(${
loadoutIdentifiers.loadoutColors.find((color) => color.hash === loadout.colorHash)
?.imagePath
})`,
}}
/>
</Grid>
))}
<Grid item md={6}>
<Button onClick={() => setIdentifiersSet(false)}>BACK</Button>
</Grid>
</Grid>
);

return (
<>
<Button onClick={() => setLoadoutDrawerOpen(true)}>SAVE IN-GAME</Button>
<Drawer open={loadoutDrawerOpen} anchor="right">
<Box sx={{ width: '24vw' }}>
{identifiersSet ? SelectLoadoutSlotDrawer : SetIdentifiersDrawer}
</Box>
</Drawer>
</>
);
}
24 changes: 24 additions & 0 deletions src/features/loadouts/hooks/use-loadout-identifiers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { useState, useEffect } from 'react';
import { db } from '../../../store/db';
import {
ManifestLoadoutColor,
ManifestLoadoutIcon,
ManifestLoadoutName,
} from '../../../types/manifest-types';

export default function useLoadoutIdentifiers() {
const [loadoutColors, setLoadoutColors] = useState<ManifestLoadoutColor[]>([]);
const [loadoutNames, setLoadoutNames] = useState<ManifestLoadoutName[]>([]);
const [loadoutIcons, setLoadoutIcons] = useState<ManifestLoadoutIcon[]>([]);

useEffect(() => {
const gatherIdentifiers = async () => {
setLoadoutColors(await db.manifestLoadoutColorDef.toArray());
setLoadoutIcons(await db.manifestLoadoutIconDef.toArray());
setLoadoutNames(await db.manifestLoadoutNameDef.toArray());
};
gatherIdentifiers().catch(console.error);
}, []);

return { loadoutNames, loadoutColors, loadoutIcons };
}
13 changes: 13 additions & 0 deletions src/features/loadouts/hooks/use-selected-character-loadouts.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { useState, useEffect } from 'react';
import { DestinyLoadout } from '../../../types/d2l-types';
import { store } from '../../../store';

export default function useSelectedCharacterLoadouts() {
const [loadouts, setLoadouts] = useState<DestinyLoadout[] | undefined>(undefined);

useEffect(() => {
setLoadouts(store.getState().profile.selectedCharacter?.loadouts);
}, []);

return loadouts;
}
8 changes: 0 additions & 8 deletions src/features/loadouts/util/loadout-utils.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,9 @@
import { ARMOR_ARRAY, DAMAGE_TYPE } from '../../../lib/bungie_api/constants';
import { snapShotLoadoutRequest } from '../../../lib/bungie_api/requests';
import { db } from '../../../store/db';
import {
armor,
armorMods,
Character,
DestinyArmor,
FilteredPermutation,
Loadout,
StatName,
Subclass,
SubclassConfig,
} from '../../../types/d2l-types';
Expand All @@ -19,9 +14,6 @@ import {
ManifestPlug,
ManifestStatPlug,
} from '../../../types/manifest-types';
import { filterPermutations } from '../../armor-optimization/filter-permutations';
import { generatePermutations } from '../../armor-optimization/generate-permutations';
import { DecodedLoadoutInfo } from '../components/findMatchingArmorSet';
import { EquipResult, setState } from '../types';
import { ArmorEquipper } from './armor-equipper';
import { SubclassEquipper } from './subclass-equipper';
Expand Down
13 changes: 13 additions & 0 deletions src/features/profile/destiny-profile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ export async function getProfileData(): Promise<ProfileData> {
const characterEquipment = response.data.Response.characterEquipment.data;
const characterData = response.data.Response.characters.data;
const profileCollectibles = response.data.Response.profileCollectibles.data.collectibles;
const characterLoadouts = response.data.Response.characterLoadouts.data;

for (const key in characterData) {
const characterClass = getCharacterClass(characterData[key].classHash);
Expand All @@ -54,8 +55,20 @@ export async function getProfileData(): Promise<ProfileData> {
},
subclasses: {},
exoticClassCombos: [],
loadouts: [],
};

// gather character's loadouts
for (const loadout of characterLoadouts[character.id].loadouts) {
character.loadouts.push({
colorHash: loadout.colorHash,
iconHash: loadout.iconHash,
nameHash: loadout.nameHash,
armor: loadout.items.slice(3, 8),
subclass: loadout.items[8],
});
}

// iterate character's equipped items
for (const item of characterEquipment[key].items) {
switch (item.bucketHash) {
Expand Down
51 changes: 51 additions & 0 deletions src/lib/bungie_api/manifest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -297,6 +297,57 @@ export async function updateManifest() {
}
}
}

const loadoutColorComponent =
response.data.Response.jsonWorldComponentContentPaths.en['DestinyLoadoutColorDefinition'];

const loadoutColorResponse = await getManifestComponentRequest(loadoutColorComponent);

if (loadoutColorResponse) {
for (const hash in loadoutColorResponse.data) {
const current = loadoutColorResponse.data[hash];

await db.manifestLoadoutColorDef.add({
imagePath: urlPrefix + current.colorImagePath,
hash: current.hash,
index: current.index,
});
}
}

const loadoutIconComponent =
response.data.Response.jsonWorldComponentContentPaths.en['DestinyLoadoutIconDefinition'];

const loadoutIconResponse = await getManifestComponentRequest(loadoutIconComponent);

if (loadoutIconResponse) {
for (const hash in loadoutIconResponse.data) {
const current = loadoutIconResponse.data[hash];

await db.manifestLoadoutIconDef.add({
imagePath: urlPrefix + current.iconImagePath,
hash: current.hash,
index: current.index,
});
}
}

const loadoutNameComponent =
response.data.Response.jsonWorldComponentContentPaths.en['DestinyLoadoutNameDefinition'];

const loadoutNameResponse = await getManifestComponentRequest(loadoutNameComponent);

if (loadoutNameResponse) {
for (const hash in loadoutNameResponse.data) {
const current = loadoutNameResponse.data[hash];

await db.manifestLoadoutNameDef.add({
name: current.name,
hash: current.hash,
index: current.index,
});
}
}
}
} else {
throw new Error('Error retrieving manifest');
Expand Down
2 changes: 1 addition & 1 deletion src/lib/bungie_api/requests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export function getProfileDataRequest(): Promise<AxiosResponse<any, any>> {
const destinyMembership = store.getState().destinyMembership.membership;

return _get(
`/Platform/Destiny2/${destinyMembership.membershipType}/Profile/${destinyMembership.membershipId}/?components=102,200,201,300,205,302,304,305,800`,
`/Platform/Destiny2/${destinyMembership.membershipType}/Profile/${destinyMembership.membershipId}/?components=102,200,201,300,205,206,302,304,305,800`,
{
headers: {
Authorization: `Bearer ${accessToken}`,
Expand Down
9 changes: 9 additions & 0 deletions src/store/db.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ import {
ManifestAspect,
ManifestStatPlug,
ManifestEntry,
ManifestLoadoutColor,
ManifestLoadoutIcon,
ManifestLoadoutName,
} from '../types/manifest-types';

const db = new Dexie('manifestDb') as Dexie & {
Expand All @@ -25,6 +28,9 @@ const db = new Dexie('manifestDb') as Dexie & {
manifestSubclassAspectsDef: EntityTable<ManifestAspect, 'itemHash'>;
manifestSubclassFragmentsDef: EntityTable<ManifestStatPlug, 'itemHash'>;
manifestSubclass: EntityTable<ManifestSubclass, 'itemHash'>;
manifestLoadoutColorDef: EntityTable<ManifestLoadoutColor, 'hash'>;
manifestLoadoutIconDef: EntityTable<ManifestLoadoutIcon, 'hash'>;
manifestLoadoutNameDef: EntityTable<ManifestLoadoutName, 'hash'>;
};

db.version(1).stores({
Expand All @@ -43,6 +49,9 @@ db.version(1).stores({
'itemHash, name, icon, secondaryIcon, category, perks, isOwned, mobilityMod, resilienceMod, recoveryMod, disciplineMod, intellectMod, strengthMod',
manifestSubclass:
'itemHash, name, icon, secondaryIcon, screenshot, flavorText, damageType, class, isOwned',
manifestLoadoutColorDef: 'hash, imagePath, index',
manifestLoadoutIconDef: 'hash, imagePath, index',
manifestLoadoutNameDef: 'hash, name, index',
});

export { db };
Loading