From 1c916a51f8a8664868b567b249d1501945be8bdb Mon Sep 17 00:00:00 2001 From: ahmedmgamal94 <98055904+ahmedmgamal94@users.noreply.github.com> Date: Thu, 22 Aug 2024 11:42:26 -0700 Subject: [PATCH] refactored abilities mod --- src/app/routes/Dashboard.tsx | 2 +- .../subclass/AbilitiesModification.tsx | 423 ++++++++++-------- src/store/LoadoutReducer.tsx | 22 +- src/types/manifest-types.ts | 14 +- 4 files changed, 242 insertions(+), 219 deletions(-) diff --git a/src/app/routes/Dashboard.tsx b/src/app/routes/Dashboard.tsx index c331f65..153e2aa 100644 --- a/src/app/routes/Dashboard.tsx +++ b/src/app/routes/Dashboard.tsx @@ -20,7 +20,7 @@ import ArmorCustomization from '../../features/armor/components/ArmorCustomizati import { resetLoadout, updateLoadoutCharacter, updateSubclass } from '../../store/LoadoutReducer'; import { ManifestSubclass } from '../../types/manifest-types'; import SubclassCustomizationWrapper from '../../features/subclass/SubclassCustomizationWrapper'; -import { updateManifest } from '../../lib/bungie_api/manifest'; +import { updateManifest } from '../../lib/bungie_api/Manifest'; const PageContainer = styled('div')({ display: 'flex', diff --git a/src/features/subclass/AbilitiesModification.tsx b/src/features/subclass/AbilitiesModification.tsx index 3d154c8..ebea61f 100644 --- a/src/features/subclass/AbilitiesModification.tsx +++ b/src/features/subclass/AbilitiesModification.tsx @@ -1,31 +1,71 @@ -import { Paper, Button, Typography, styled } from '@mui/material'; -import { Box, Container } from '@mui/system'; import React, { useCallback, useEffect, useState } from 'react'; import { useSelector, useDispatch } from 'react-redux'; -import { PLUG_CATEGORY_HASH } from '../../lib/bungie_api/constants'; +import { Paper, Button, Typography, styled, CircularProgress } from '@mui/material'; +import { Box, Container } from '@mui/system'; +import { PLUG_CATEGORY_HASH } from '../../lib/bungie_api/subclass-constants'; import { RootState } from '../../store'; import { db } from '../../store/db'; import { updateSubclassMods } from '../../store/LoadoutReducer'; -import { Plug } from '../../types/d2l-types'; -import { ManifestSubclass, ManifestPlug } from '../../types/manifest-types'; +import { + ManifestSubclass, + ManifestPlug, + ManifestAspect, + ManifestStatPlug, +} from '../../types/manifest-types'; +import { DamageType } from '../../types/d2l-types'; interface AbilitiesModificationProps { subclass: ManifestSubclass; } -const EMPTY_PLUG: Plug = { - plugItemHash: '', - socketArrayType: 0, - socketIndex: -1, +export const EMPTY_MANIFEST_PLUG: ManifestPlug = { + perkName: '', + perkDescription: '', + perkIcon: '', + category: 0, + isOwned: false, + itemHash: 0, + name: '', + icon: '', }; -const subclassTypeMap: { [key: number]: string } = { +export const EMPTY_ASPECT: ManifestAspect = { + energyCapacity: 0, + perkName: '', + perkDescription: '', + perkIcon: '', + category: 0, + isOwned: false, + itemHash: 0, + name: '', + icon: '', +}; + +export const EMPTY_FRAGMENT: ManifestStatPlug = { + mobilityMod: 0, + resilienceMod: 0, + recoveryMod: 0, + disciplineMod: 0, + intellectMod: 0, + strengthMod: 0, + perkName: '', + perkDescription: '', + perkIcon: '', + category: 0, + isOwned: false, + itemHash: 0, + name: '', + icon: '', +}; + +const subclassTypeMap: { [key in DamageType]: string } = { 1: 'PRISMATIC', 2: 'ARC', 3: 'SOLAR', 4: 'VOID', 6: 'STASIS', 7: 'STRAND', + 5: '', }; const ModSlot = styled(Paper)(({ theme }) => ({ @@ -105,215 +145,179 @@ const StyledTitle = styled(Typography)(({ theme }) => ({ width: '40%', })); -const getCategoryHashes = (subclass: ManifestSubclass) => { - const subclassType = subclassTypeMap[ - subclass.damageType - ] as keyof typeof PLUG_CATEGORY_HASH.TITAN.ARC; - const classType = subclass.class.toUpperCase() as 'TITAN' | 'HUNTER' | 'WARLOCK'; - - const classAndSubclass = - (PLUG_CATEGORY_HASH[classType] as Record)[subclassType] || {}; - - const categoryHashes = { - SUPERS: Object.values(classAndSubclass.SUPERS || []), - CLASS_ABILITIES: Object.values(classAndSubclass.CLASS_ABILITIES || []), - MOVEMENT_ABILITIES: Object.values(classAndSubclass.MOVEMENT_ABILITIES || []), - MELEE_ABILITIES: Object.values(classAndSubclass.MELEE_ABILITIES || []), - GRENADES: Object.values(classAndSubclass.GRENADES || []), - ASPECTS: Object.values(classAndSubclass.ASPECTS || []), - FRAGMENTS: Object.values(classAndSubclass.FRAGMENTS || []), - }; - - return categoryHashes; -}; - const fetchMods = async (subclass: ManifestSubclass) => { - const categoryHashes = getCategoryHashes(subclass); - const modsData: { [key: string]: ManifestPlug[] } = {}; - - await Promise.all( - Object.entries(categoryHashes).map(async ([key, hashes]) => { - const typedHashes = hashes as number[]; - const mods = await db.manifestSubclassModDef.where('category').anyOf(typedHashes).toArray(); - modsData[key] = Array.from(new Set(mods.map((mod) => mod.itemHash))).map((itemHash) => - mods.find((mod) => mod.itemHash === itemHash) - ) as ManifestPlug[]; - }) - ); + const modsData: { [key: string]: (ManifestPlug | ManifestAspect | ManifestStatPlug)[] } = { + SUPERS: [], + CLASS_ABILITIES: [], + MOVEMENT_ABILITIES: [], + MELEE_ABILITIES: [], + GRENADES: [], + ASPECTS: [], + FRAGMENTS: [], + }; - return modsData; + const classType = subclass.class.toUpperCase() as keyof typeof PLUG_CATEGORY_HASH; + const damageType = subclassTypeMap[ + subclass.damageType as DamageType + ] as keyof (typeof PLUG_CATEGORY_HASH)[typeof classType]; + + if (!PLUG_CATEGORY_HASH[classType] || !PLUG_CATEGORY_HASH[classType][damageType]) { + console.error(`Invalid class type ${classType} or damage type ${damageType}`); + return modsData; + } + + const categoryHashes = PLUG_CATEGORY_HASH[classType][damageType]; + + try { + const queries = [ + { category: 'SUPERS', table: db.manifestSubclassModDef, hash: categoryHashes.SUPERS }, + { + category: 'CLASS_ABILITIES', + table: db.manifestSubclassModDef, + hash: categoryHashes.CLASS_ABILITIES, + }, + { + category: 'MOVEMENT_ABILITIES', + table: db.manifestSubclassModDef, + hash: categoryHashes.MOVEMENT_ABILITIES, + }, + { + category: 'MELEE_ABILITIES', + table: db.manifestSubclassModDef, + hash: categoryHashes.MELEE_ABILITIES, + }, + { category: 'GRENADES', table: db.manifestSubclassModDef, hash: categoryHashes.GRENADES }, + { category: 'ASPECTS', table: db.manifestSubclassAspectsDef, hash: categoryHashes.ASPECTS }, + { + category: 'FRAGMENTS', + table: db.manifestSubclassFragmentsDef, + hash: categoryHashes.FRAGMENTS, + }, + ]; + + await Promise.all( + queries.map(async ({ category, table, hash }) => { + const hashValues = Object.values(hash); + const results = await table.where('category').anyOf(hashValues).toArray(); + modsData[category] = results; + }) + ); + + return modsData; + } catch (error) { + console.error('Error fetching mods:', error); + return modsData; + } }; const AbilitiesModification: React.FC = ({ subclass }) => { - const [mods, setMods] = useState<{ [key: string]: ManifestPlug[] }>({}); - const [selectedMods, setSelectedMods] = useState<{ [key: string]: ManifestPlug[] }>({}); - const [modIcons, setModIcons] = useState<{ [key: string]: string }>({}); - const loadout = useSelector((state: RootState) => state.loadoutConfig.loadout.subclassConfig); + const [mods, setMods] = useState<{ + [key: string]: (ManifestPlug | ManifestAspect | ManifestStatPlug)[]; + }>({}); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [hoveredSlot, setHoveredSlot] = useState(null); + const [submenuPosition, setSubmenuPosition] = useState({ top: 0, left: 0 }); const dispatch = useDispatch(); + const loadout = useSelector((state: RootState) => state.loadoutConfig.loadout.subclassConfig); useEffect(() => { if (subclass) { - fetchMods(subclass).then((fetchedMods) => { - setMods(fetchedMods); - - const initialSelectedMods: { [key: string]: ManifestPlug[] } = {}; - Object.keys(fetchedMods).forEach((category) => { - initialSelectedMods[category] = []; - if (loadout) { - switch (category) { - case 'SUPERS': - initialSelectedMods[category] = [ - fetchedMods[category].find( - (mod) => String(mod.itemHash) === loadout.super.plugItemHash - ), - ].filter(Boolean) as ManifestPlug[]; - break; - case 'ASPECTS': - initialSelectedMods[category] = loadout.aspects - .map((aspect) => - fetchedMods[category].find( - (mod) => String(mod.itemHash) === aspect.plugItemHash - ) - ) - .filter(Boolean) as ManifestPlug[]; - break; - case 'FRAGMENTS': - initialSelectedMods[category] = loadout.fragments - .map((fragment) => - fetchedMods[category].find( - (mod) => String(mod.itemHash) === fragment.plugItemHash - ) - ) - .filter(Boolean) as ManifestPlug[]; - break; - case 'CLASS_ABILITIES': - if (loadout.classAbility) { - initialSelectedMods[category] = [ - fetchedMods[category].find( - (mod) => String(mod.itemHash) === loadout.classAbility?.plugItemHash - ), - ].filter(Boolean) as ManifestPlug[]; - } - break; - case 'MOVEMENT_ABILITIES': - if (loadout.movementAbility) { - initialSelectedMods[category] = [ - fetchedMods[category].find( - (mod) => String(mod.itemHash) === loadout.movementAbility?.plugItemHash - ), - ].filter(Boolean) as ManifestPlug[]; - } - break; - case 'MELEE_ABILITIES': - if (loadout.meleeAbility) { - initialSelectedMods[category] = [ - fetchedMods[category].find( - (mod) => String(mod.itemHash) === loadout.meleeAbility?.plugItemHash - ), - ].filter(Boolean) as ManifestPlug[]; - } - break; - case 'GRENADES': - if (loadout.grenade) { - initialSelectedMods[category] = [ - fetchedMods[category].find( - (mod) => String(mod.itemHash) === loadout.grenade?.plugItemHash - ), - ].filter(Boolean) as ManifestPlug[]; - } - break; - } - } + setLoading(true); + setError(null); + fetchMods(subclass) + .then((modsData) => { + setMods(modsData); + setLoading(false); + }) + .catch((err) => { + setError('Failed to fetch mods. Please try again.'); + setLoading(false); }); - setSelectedMods(initialSelectedMods); - }); } - }, [subclass, loadout]); - - const fetchModIcon = useCallback( - async (plugItemHash: string): Promise => { - if (modIcons[plugItemHash]) return modIcons[plugItemHash]; - - const mod = await db.manifestSubclassModDef - .where('itemHash') - .equals(Number(plugItemHash)) - .first(); - if (mod && mod.icon) { - setModIcons((prev) => ({ ...prev, [plugItemHash]: mod.icon })); - return mod.icon; - } - return ''; - }, - [modIcons] - ); + }, [subclass]); + + const handleModSelect = ( + category: string, + mod: ManifestPlug | ManifestAspect | ManifestStatPlug, + index?: number + ) => { + let updatedMods; + + switch (category) { + case 'ASPECTS': + updatedMods = [...loadout.aspects]; + break; + case 'FRAGMENTS': + updatedMods = [...loadout.fragments]; + break; + case 'SUPERS': + case 'CLASS_ABILITIES': + case 'MOVEMENT_ABILITIES': + case 'MELEE_ABILITIES': + case 'GRENADES': + updatedMods = [mod]; + break; + default: + return; + } - const handleModSelect = (category: string, mod: ManifestPlug, index?: number) => { - let payload; - - if (category === 'ASPECTS') { - const newMods = [...loadout.aspects]; - if (index !== undefined && index < 2) { - // Remove the mod from its previous position if it exists elsewhere - const existingIndex = newMods.findIndex((m) => m?.plugItemHash === String(mod.itemHash)); - if (existingIndex !== -1) { - newMods[existingIndex] = EMPTY_PLUG; - } - newMods[index] = { plugItemHash: String(mod.itemHash) }; - } - payload = { category, mods: newMods }; - } else if (category === 'FRAGMENTS') { - const newMods = Array(5) - .fill(EMPTY_PLUG) - .map((plug, i) => loadout.fragments[i] || plug); - if (index !== undefined && index < 5) { - // Remove the mod from its previous position if it exists elsewhere - const existingIndex = newMods.findIndex((m) => m?.plugItemHash === String(mod.itemHash)); - if (existingIndex !== -1) { - newMods[existingIndex] = EMPTY_PLUG; - } - newMods[index] = { plugItemHash: String(mod.itemHash) }; + // Check if the mod already exists in another slot and replace it with an empty mod + if (category === 'ASPECTS' || category === 'FRAGMENTS') { + const modIndex = updatedMods.findIndex( + (existingMod) => existingMod.itemHash === mod.itemHash + ); + + if (modIndex !== -1 && modIndex !== index) { + updatedMods[modIndex] = category === 'ASPECTS' ? EMPTY_ASPECT : EMPTY_FRAGMENT; } - payload = { category, mods: newMods }; + + // Assign the mod to the selected slot + updatedMods[index!] = mod; } else { - payload = { category, mods: [{ plugItemHash: String(mod.itemHash) }] }; + // Directly assign the mod for SUPERS and abilities + updatedMods[0] = mod; } - dispatch(updateSubclassMods(payload)); + dispatch( + updateSubclassMods({ + category, + mods: updatedMods, + }) + ); + }; + + const handleMouseEnter = (event: React.MouseEvent, slotId: string) => { + const rect = event.currentTarget.getBoundingClientRect(); + setSubmenuPosition({ + top: rect.bottom + window.scrollY, + left: rect.left + window.scrollX, + }); + setHoveredSlot(slotId); + }; + + const handleMouseLeave = () => { + setHoveredSlot(null); }; const renderModCategory = useCallback( - (category: string, currentMod: Plug | null, index?: number) => { - const [currentModIcon, setCurrentModIcon] = useState(null); - const [isHovered, setIsHovered] = useState(false); - const [submenuPosition, setSubmenuPosition] = useState({ top: 0, left: 0 }); - - useEffect(() => { - if (currentMod) { - fetchModIcon(currentMod.plugItemHash).then(setCurrentModIcon); - } else { - setCurrentModIcon(null); - } - }, [currentMod]); - - const isEmptyMod = !currentMod || currentMod.plugItemHash === ''; - - const handleMouseEnter = (event: React.MouseEvent) => { - const rect = event.currentTarget.getBoundingClientRect(); - setSubmenuPosition({ - top: rect.bottom + window.scrollY, - left: rect.left + window.scrollX, - }); - setIsHovered(true); - }; + ( + category: string, + currentMod: ManifestPlug | ManifestAspect | ManifestStatPlug | null, + index?: number + ) => { + const isEmptyMod = !currentMod || currentMod.itemHash === 0; + const slotId = `${category}-${index ?? 0}`; + const isHovered = hoveredSlot === slotId; const SlotComponent = category === 'SUPERS' ? SuperModSlot : ModSlot; return ( - -
setIsHovered(false)}> + +
handleMouseEnter(e, slotId)} onMouseLeave={handleMouseLeave}> {isEmptyMod && ( @@ -343,8 +347,29 @@ const AbilitiesModification: React.FC = ({ subclass ); }, - [mods, handleModSelect, fetchModIcon] + [mods, handleModSelect, hoveredSlot, submenuPosition] ); + + if (loading) { + return ( + + + + + + ); + } + + if (error) { + return ( + + + {error} + + + ); + } + return (
@@ -377,9 +402,9 @@ const AbilitiesModification: React.FC = ({ subclass ASPECTS - {[0, 1].map((index) => ( + {loadout.aspects.map((aspect, index) => ( - {renderModCategory('ASPECTS', loadout.aspects[index] || EMPTY_PLUG, index)} + {renderModCategory('ASPECTS', aspect, index)} ))} @@ -388,9 +413,9 @@ const AbilitiesModification: React.FC = ({ subclass FRAGMENTS - {[0, 1, 2, 3, 4].map((index) => ( + {loadout.fragments.map((fragment, index) => ( - {renderModCategory('FRAGMENTS', loadout.fragments[index] || EMPTY_PLUG, index)} + {renderModCategory('FRAGMENTS', fragment, index)} ))} diff --git a/src/store/LoadoutReducer.tsx b/src/store/LoadoutReducer.tsx index 572dfb1..0dec289 100644 --- a/src/store/LoadoutReducer.tsx +++ b/src/store/LoadoutReducer.tsx @@ -472,36 +472,34 @@ export const loadoutConfigSlice = createSlice({ }, updateSubclassMods: (state, action: PayloadAction<{ category: string; mods: any[] }>) => { const { category, mods } = action.payload; + switch (category) { case 'SUPERS': - state.loadout.subclassConfig.super = mods[0] ? mods[0] : EMPTY_MANIFEST_PLUG; + state.loadout.subclassConfig.super = mods[0] || EMPTY_MANIFEST_PLUG; break; case 'ASPECTS': mods.forEach((mod, index) => { - state.loadout.subclassConfig.aspects[ - index + (state.loadout.subclassConfig.damageType === DAMAGE_TYPE.KINETIC ? 7 : 5) - ] = mod ? mod : EMPTY_ASPECT; + state.loadout.subclassConfig.aspects[index] = mod || EMPTY_ASPECT; }); - break; case 'FRAGMENTS': mods.forEach((mod, index) => { - state.loadout.subclassConfig.fragments[ - index + (state.loadout.subclassConfig.damageType === DAMAGE_TYPE.KINETIC ? 9 : 7) - ] = mod ? mod : EMPTY_FRAGMENT; + state.loadout.subclassConfig.fragments[index] = mod || EMPTY_FRAGMENT; }); break; case 'CLASS_ABILITIES': - state.loadout.subclassConfig.classAbility = mods[0] ? mods[0] : null; + state.loadout.subclassConfig.classAbility = mods[0] || null; break; case 'MELEE_ABILITIES': - state.loadout.subclassConfig.meleeAbility = mods[0] ? mods[0] : null; + state.loadout.subclassConfig.meleeAbility = mods[0] || null; break; case 'MOVEMENT_ABILITIES': - state.loadout.subclassConfig.movementAbility = mods[0] ? mods[0] : null; + state.loadout.subclassConfig.movementAbility = mods[0] || null; break; case 'GRENADES': - state.loadout.subclassConfig.grenade = mods[0] ? mods[0] : null; + state.loadout.subclassConfig.grenade = mods[0] || null; + break; + default: break; } }, diff --git a/src/types/manifest-types.ts b/src/types/manifest-types.ts index 42ff566..1da8105 100644 --- a/src/types/manifest-types.ts +++ b/src/types/manifest-types.ts @@ -36,16 +36,16 @@ export interface ManifestPlug extends ManifestEntry { } export interface ManifestStatPlug extends ManifestPlug { - mobilityMod: number; - resilienceMod: number; - recoveryMod: number; - disciplineMod: number; - intellectMod: number; - strengthMod: number; + mobilityMod?: number; + resilienceMod?: number; + recoveryMod?: number; + disciplineMod?: number; + intellectMod?: number; + strengthMod?: number; } export interface ManifestAspect extends ManifestPlug { - energyCapacity: number; + energyCapacity?: number; } export interface ManifestArmorMod extends ManifestPlug {