From f0b18290b7912e9c55d8551db8e54cf4217eab3a Mon Sep 17 00:00:00 2001 From: ECWireless Date: Mon, 26 Aug 2024 14:55:52 -0600 Subject: [PATCH 1/3] Fix bug character creation game board redirect --- packages/client/src/pages/CharacterCreation.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/client/src/pages/CharacterCreation.tsx b/packages/client/src/pages/CharacterCreation.tsx index adfd39715..065864109 100644 --- a/packages/client/src/pages/CharacterCreation.tsx +++ b/packages/client/src/pages/CharacterCreation.tsx @@ -386,7 +386,6 @@ export const CharacterCreation = (): JSX.Element => { if (character && rolledOnce) { setCharacterClass(character.entityClass); - return; } if (character?.locked) { From 94c3fe13df94609e66d8ee3fe05d869c360bd098 Mon Sep 17 00:00:00 2001 From: ECWireless Date: Mon, 26 Aug 2024 20:25:58 -0600 Subject: [PATCH 2/3] Add ItemsContext --- .../client/src/contexts/CharacterContext.tsx | 137 +++-------- packages/client/src/contexts/ItemsContext.tsx | 227 ++++++++++++++++++ packages/client/src/index.tsx | 9 +- packages/client/src/pages/AuctionHouse.tsx | 20 +- 4 files changed, 272 insertions(+), 121 deletions(-) create mode 100644 packages/client/src/contexts/ItemsContext.tsx diff --git a/packages/client/src/contexts/CharacterContext.tsx b/packages/client/src/contexts/CharacterContext.tsx index 08acbf3a3..b9db83dcb 100644 --- a/packages/client/src/contexts/CharacterContext.tsx +++ b/packages/client/src/contexts/CharacterContext.tsx @@ -4,7 +4,7 @@ import { HasValue, runQuery, } from '@latticexyz/recs'; -import { encodeEntity, singletonEntity } from '@latticexyz/store-sync/recs'; +import { encodeEntity } from '@latticexyz/store-sync/recs'; import { createContext, ReactNode, @@ -16,12 +16,7 @@ import { import { formatEther, hexToString, zeroHash } from 'viem'; import { useToast } from '../hooks/useToast'; -import { - decodeArmorStats, - decodeWeaponStats, - fetchMetadataFromUri, - uriToHttp, -} from '../utils/helpers'; +import { fetchMetadataFromUri, uriToHttp } from '../utils/helpers'; import type { Armor, Character, @@ -29,6 +24,7 @@ import type { EntityStats, Weapon, } from '../utils/types'; +import { useItems } from './ItemsContext'; import { useMUD } from './MUDContext'; type CharacterContextType = { @@ -61,10 +57,7 @@ export const CharacterProvider = ({ CharactersTokenURI, EncounterEntity, GoldBalances, - Items, - ItemsBaseURI, ItemsOwners, - ItemsTokenURI, Stats, }, delegatorAddress, @@ -72,6 +65,7 @@ export const CharacterProvider = ({ network: { publicClient, worldContract }, } = useMUD(); const { renderError } = useToast(); + const { armor, isLoading: isLoadingItems, weapons } = useItems(); const [userCharacter, setUserCharacter] = useState(null); const [isRefreshing, setIsRefreshing] = useState(true); @@ -209,12 +203,16 @@ export const CharacterProvider = ({ tokenOwnersEntity, ); + const armorDetails = armor.find( + item => item.tokenId === tokenId.toString(), + ); + return { + ...armorDetails, balance: itemOwner.balance.toString(), itemId: tokenOwnersEntity, owner: _character.owner, - tokenId: tokenId.toString(), - }; + } as Armor; }); const _weapons = _equippedWeapons @@ -231,113 +229,25 @@ export const CharacterProvider = ({ tokenOwnersEntity, ); + const weaponDetails = weapons.find( + item => item.tokenId === tokenId.toString(), + ); + return { + ...weaponDetails, balance: itemOwner.balance.toString(), itemId: tokenOwnersEntity, owner: _character.owner, tokenId: tokenId.toString(), - }; + } as Weapon; }) .filter(item => item.owner === _character.owner) .sort((a, b) => { return Number(a.tokenId) - Number(b.tokenId); }); - const fullArmor = await Promise.all( - _armor.map(async item => { - const tokenIdEntity = encodeEntity( - { tokenId: 'uint256' }, - { tokenId: BigInt(item.tokenId) }, - ); - - const itemTemplate = getComponentValueStrict(Items, tokenIdEntity); - const decodedArmorStats = decodeArmorStats(itemTemplate.stats); - - const baseURI = getComponentValueStrict( - ItemsBaseURI, - singletonEntity, - ).uri; - - const tokenURI = getComponentValueStrict( - ItemsTokenURI, - tokenIdEntity, - ).uri; - - const metadata = await fetchMetadataFromUri( - uriToHttp(`${baseURI}${tokenURI}`)[0], - ); - - return { - ...metadata, - agiModifier: decodedArmorStats.agiModifier, - armorModifier: decodedArmorStats.armorModifier, - balance: item.balance, - hitPointModifier: decodedArmorStats.hitPointModifier, - intModifier: decodedArmorStats.intModifier, - itemId: item.itemId, - minLevel: decodedArmorStats.minLevel, - owner: item.owner, - statRestrictions: { - minAgility: decodedArmorStats.statRestrictions.minAgility, - minIntelligence: - decodedArmorStats.statRestrictions.minIntelligence, - minStrength: decodedArmorStats.statRestrictions.minStrength, - }, - strModifier: decodedArmorStats.strModifier, - tokenId: item.tokenId, - } as Armor; - }), - ); - - const fullWeapons = await Promise.all( - _weapons.map(async item => { - const tokenIdEntity = encodeEntity( - { tokenId: 'uint256' }, - { tokenId: BigInt(item.tokenId) }, - ); - - const itemTemplate = getComponentValueStrict(Items, tokenIdEntity); - const decodedWeaponStats = decodeWeaponStats(itemTemplate.stats); - - const baseURI = getComponentValueStrict( - ItemsBaseURI, - singletonEntity, - ).uri; - - const tokenURI = getComponentValueStrict( - ItemsTokenURI, - tokenIdEntity, - ).uri; - - const metadata = await fetchMetadataFromUri( - uriToHttp(`${baseURI}${tokenURI}`)[0], - ); - - return { - ...metadata, - agiModifier: decodedWeaponStats.agiModifier, - balance: item.balance, - hitPointModifier: decodedWeaponStats.hitPointModifier, - intModifier: decodedWeaponStats.intModifier, - itemId: item.itemId, - maxDamage: decodedWeaponStats.maxDamage, - minDamage: decodedWeaponStats.minDamage, - minLevel: decodedWeaponStats.minLevel, - owner: item.owner, - statRestrictions: { - minAgility: decodedWeaponStats.statRestrictions.minAgility, - minIntelligence: - decodedWeaponStats.statRestrictions.minIntelligence, - minStrength: decodedWeaponStats.statRestrictions.minStrength, - }, - strModifier: decodedWeaponStats.strModifier, - tokenId: item.tokenId, - } as Weapon; - }), - ); - - setEquippedArmor(fullArmor); - setEquippedWeapons(fullWeapons); + setEquippedArmor(_armor); + setEquippedWeapons(_weapons); } catch (e) { renderError( (e as Error)?.message ?? 'Failed to fetch character data.', @@ -345,11 +255,12 @@ export const CharacterProvider = ({ ); } }, - [Items, ItemsBaseURI, ItemsOwners, ItemsTokenURI, renderError], + [armor, ItemsOwners, renderError, weapons], ); useEffect(() => { if (!isSynced) return; + if (isLoadingItems) return; (async (): Promise => { if (!userCharacter) return; @@ -361,7 +272,13 @@ export const CharacterProvider = ({ }); await fetchCharacterItems(userCharacter, equippedArmor, equippedWeapons); })(); - }, [userCharacter, CharacterEquipment, fetchCharacterItems, isSynced]); + }, [ + CharacterEquipment, + fetchCharacterItems, + isLoadingItems, + isSynced, + userCharacter, + ]); return ( ({ + armor: [], + weapons: [], + isLoading: false, +}); + +export const ItemsProvider = ({ + children, +}: { + children: ReactNode; +}): JSX.Element => { + const { renderError } = useToast(); + const { + components: { Items, ItemsBaseURI, ItemsTokenURI }, + isSynced, + } = useMUD(); + + const [armor, setArmor] = useState([]); + const [weapons, setWeapons] = useState([]); + const [isLoading, setIsLoading] = useState(true); + + const fetchAllArmor = useCallback( + async (allArmorIds: bigint[]) => { + const fullArmor = await Promise.all( + allArmorIds.map(async armorId => { + const tokenOwnersEntity = encodeEntity( + { owner: 'address', tokenId: 'uint256' }, + { + owner: zeroAddress, + tokenId: armorId, + }, + ); + + const tokenIdEntity = encodeEntity( + { tokenId: 'uint256' }, + { tokenId: armorId }, + ); + + const itemTemplate = getComponentValueStrict(Items, tokenIdEntity); + const decodedArmorStats = decodeArmorStats(itemTemplate.stats); + + const baseURI = getComponentValueStrict( + ItemsBaseURI, + singletonEntity, + ).uri; + + const tokenURI = getComponentValueStrict( + ItemsTokenURI, + tokenIdEntity, + ).uri; + + const metadata = await fetchMetadataFromUri( + uriToHttp(`${baseURI}${tokenURI}`)[0], + ); + + return { + ...metadata, + agiModifier: decodedArmorStats.agiModifier, + armorModifier: decodedArmorStats.armorModifier, + balance: '0', + hitPointModifier: decodedArmorStats.hitPointModifier, + intModifier: decodedArmorStats.intModifier, + itemId: tokenOwnersEntity, + minLevel: decodedArmorStats.minLevel, + owner: zeroAddress, + statRestrictions: { + minAgility: decodedArmorStats.statRestrictions.minAgility, + minIntelligence: + decodedArmorStats.statRestrictions.minIntelligence, + minStrength: decodedArmorStats.statRestrictions.minStrength, + }, + strModifier: decodedArmorStats.strModifier, + tokenId: armorId.toString(), + } as Armor; + }), + ); + + return fullArmor; + }, + [Items, ItemsBaseURI, ItemsTokenURI], + ); + + const fetchAllWeapons = useCallback( + async (allWeaponIds: bigint[]) => { + const fullWeapons = await Promise.all( + allWeaponIds.map(async weaponId => { + const tokenOwnersEntity = encodeEntity( + { owner: 'address', tokenId: 'uint256' }, + { + owner: zeroAddress, + tokenId: weaponId, + }, + ); + + const tokenIdEntity = encodeEntity( + { tokenId: 'uint256' }, + { tokenId: weaponId }, + ); + + const itemTemplate = getComponentValueStrict(Items, tokenIdEntity); + const decodedArmorStats = decodeWeaponStats(itemTemplate.stats); + + const baseURI = getComponentValueStrict( + ItemsBaseURI, + singletonEntity, + ).uri; + + const tokenURI = getComponentValueStrict( + ItemsTokenURI, + tokenIdEntity, + ).uri; + + const metadata = await fetchMetadataFromUri( + uriToHttp(`${baseURI}${tokenURI}`)[0], + ); + + return { + ...metadata, + agiModifier: decodedArmorStats.agiModifier, + balance: '0', + hitPointModifier: decodedArmorStats.hitPointModifier, + intModifier: decodedArmorStats.intModifier, + itemId: tokenOwnersEntity, + maxDamage: decodedArmorStats.maxDamage, + minDamage: decodedArmorStats.minDamage, + minLevel: decodedArmorStats.minLevel, + owner: zeroAddress, + statRestrictions: { + minAgility: decodedArmorStats.statRestrictions.minAgility, + minIntelligence: + decodedArmorStats.statRestrictions.minIntelligence, + minStrength: decodedArmorStats.statRestrictions.minStrength, + }, + strModifier: decodedArmorStats.strModifier, + tokenId: weaponId.toString(), + } as Weapon; + }), + ); + + return fullWeapons; + }, + [Items, ItemsBaseURI, ItemsTokenURI], + ); + + useEffect(() => { + (async () => { + if (!isSynced) return; + + try { + const allItemIds = Array.from(runQuery([Has(Items)])).map(entity => { + const itemTemplate = getComponentValueStrict(Items, entity); + const { tokenId } = decodeEntity({ tokenId: 'uint256' }, entity); + return { + itemType: itemTemplate.itemType, + tokenId, + }; + }); + + if (allItemIds.length > 0) { + const allArmorIds = allItemIds + .filter(({ itemType }) => itemType === ItemType.Armor) + .map(({ tokenId }) => tokenId); + + const _armor = await fetchAllArmor(allArmorIds); + setArmor(_armor); + + const allWeaponIds = allItemIds + .filter(({ itemType }) => itemType === ItemType.Weapon) + .map(({ tokenId }) => tokenId); + + const _weapons = await fetchAllWeapons(allWeaponIds); + setWeapons(_weapons); + } + } catch (e) { + renderError((e as Error)?.message ?? 'Failed to fetch items.', e); + } finally { + setIsLoading(false); + } + })(); + }, [fetchAllArmor, fetchAllWeapons, isSynced, Items, renderError]); + + return ( + + {children} + + ); +}; + +export const useItems = (): ItemsContextType => useContext(ItemsContext); diff --git a/packages/client/src/index.tsx b/packages/client/src/index.tsx index e4ea6f209..c72e546ce 100644 --- a/packages/client/src/index.tsx +++ b/packages/client/src/index.tsx @@ -14,6 +14,7 @@ import { createRoot } from 'react-dom/client'; import { App } from './App'; import { DevTools } from './components/DevTools'; import { CharacterProvider } from './contexts/CharacterContext'; +import { ItemsProvider } from './contexts/ItemsContext'; import { MUDProvider } from './contexts/MUDContext'; import { Web3Provider } from './contexts/Web3Provider'; import { setup } from './lib/mud/setup'; @@ -30,9 +31,11 @@ setup().then(async result => { - - - + + + + + {import.meta.env.DEV && } diff --git a/packages/client/src/pages/AuctionHouse.tsx b/packages/client/src/pages/AuctionHouse.tsx index c018c343d..85e2393c6 100644 --- a/packages/client/src/pages/AuctionHouse.tsx +++ b/packages/client/src/pages/AuctionHouse.tsx @@ -240,10 +240,6 @@ export const AuctionHouse = (): JSX.Element => { : data.class; data.stats = { agiModifier: w.agiModifier.toString(), - classRestrictions: w.classRestrictions.map( - (classRestriction: number) => - classRestriction as StatsClasses, - ), hitPointModifier: w.hitPointModifier.toString(), intModifier: w.intModifier.toString(), itemId: item.itemId, @@ -251,6 +247,12 @@ export const AuctionHouse = (): JSX.Element => { minDamage: w.minDamage.toString(), minLevel: w.minLevel.toString(), owner: item.owner, + statRestrictions: { + minAgility: w.statRestrictions.minAgility.toString(), + minIntelligence: + w.statRestrictions.minIntelligence.toString(), + minStrength: w.statRestrictions.minStrength.toString(), + }, strModifier: w.strModifier.toString(), tokenId: item.tokenId, } as WeaponStats; @@ -283,15 +285,17 @@ export const AuctionHouse = (): JSX.Element => { data.stats = { armorModifier: a.armorModifier.toString(), agiModifier: a.agiModifier.toString(), - classRestrictions: a.classRestrictions.map( - (classRestriction: number) => - classRestriction as StatsClasses, - ), hitPointModifier: a.hitPointModifier.toString(), intModifier: a.intModifier.toString(), itemId: item.itemId, minLevel: a.minLevel.toString(), owner: item.owner, + statRestrictions: { + minAgility: a.statRestrictions.minAgility.toString(), + minIntelligence: + a.statRestrictions.minIntelligence.toString(), + minStrength: a.statRestrictions.minStrength.toString(), + }, strModifier: a.strModifier.toString(), tokenId: item.tokenId, } as ArmorStats; From edbac41e5c4a958bb779a915e95e4ca89666e8e5 Mon Sep 17 00:00:00 2001 From: ECWireless Date: Mon, 26 Aug 2024 22:19:48 -0600 Subject: [PATCH 3/3] Use ItemsContext throughout app --- .../client/src/contexts/CharacterContext.tsx | 37 ++-- packages/client/src/contexts/ItemsContext.tsx | 20 +- packages/client/src/pages/Character.tsx | 175 +++++------------- .../client/src/pages/CharacterCreation.tsx | 145 +++------------ 4 files changed, 102 insertions(+), 275 deletions(-) diff --git a/packages/client/src/contexts/CharacterContext.tsx b/packages/client/src/contexts/CharacterContext.tsx index b9db83dcb..fbfd6a1d4 100644 --- a/packages/client/src/contexts/CharacterContext.tsx +++ b/packages/client/src/contexts/CharacterContext.tsx @@ -65,7 +65,11 @@ export const CharacterProvider = ({ network: { publicClient, worldContract }, } = useMUD(); const { renderError } = useToast(); - const { armor, isLoading: isLoadingItems, weapons } = useItems(); + const { + armorTemplates, + isLoading: isLoadingItemTemplates, + weaponTemplates, + } = useItems(); const [userCharacter, setUserCharacter] = useState(null); const [isRefreshing, setIsRefreshing] = useState(true); @@ -173,7 +177,7 @@ export const CharacterProvider = ({ ]); const fetchCharacterItems = useCallback( - async ( + ( _character: Character, _equippedArmor: bigint[], _equippedWeapons: bigint[], @@ -203,7 +207,7 @@ export const CharacterProvider = ({ tokenOwnersEntity, ); - const armorDetails = armor.find( + const armorDetails = armorTemplates.find( item => item.tokenId === tokenId.toString(), ); @@ -229,7 +233,7 @@ export const CharacterProvider = ({ tokenOwnersEntity, ); - const weaponDetails = weapons.find( + const weaponDetails = weaponTemplates.find( item => item.tokenId === tokenId.toString(), ); @@ -255,27 +259,24 @@ export const CharacterProvider = ({ ); } }, - [armor, ItemsOwners, renderError, weapons], + [armorTemplates, ItemsOwners, renderError, weaponTemplates], ); useEffect(() => { - if (!isSynced) return; - if (isLoadingItems) return; - (async (): Promise => { - if (!userCharacter) return; + if (!(isSynced && userCharacter) || isLoadingItemTemplates) return; - const { equippedArmor, equippedWeapons } = - getComponentValue(CharacterEquipment, userCharacter.id) ?? - ({ equippedArmor: [], equippedWeapons: [] } as { - equippedArmor: bigint[]; - equippedWeapons: bigint[]; - }); - await fetchCharacterItems(userCharacter, equippedArmor, equippedWeapons); - })(); + const { equippedArmor, equippedWeapons } = + getComponentValue(CharacterEquipment, userCharacter.id) ?? + ({ equippedArmor: [], equippedWeapons: [] } as { + equippedArmor: bigint[]; + equippedWeapons: bigint[]; + }); + + fetchCharacterItems(userCharacter, equippedArmor, equippedWeapons); }, [ CharacterEquipment, fetchCharacterItems, - isLoadingItems, + isLoadingItemTemplates, isSynced, userCharacter, ]); diff --git a/packages/client/src/contexts/ItemsContext.tsx b/packages/client/src/contexts/ItemsContext.tsx index 5e458dcb7..67ad9649b 100644 --- a/packages/client/src/contexts/ItemsContext.tsx +++ b/packages/client/src/contexts/ItemsContext.tsx @@ -25,14 +25,14 @@ import { type Armor, ItemType, type Weapon } from '../utils/types'; import { useMUD } from './MUDContext'; type ItemsContextType = { - armor: Armor[]; - weapons: Weapon[]; + armorTemplates: Armor[]; + weaponTemplates: Weapon[]; isLoading: boolean; }; const ItemsContext = createContext({ - armor: [], - weapons: [], + armorTemplates: [], + weaponTemplates: [], isLoading: false, }); @@ -47,8 +47,8 @@ export const ItemsProvider = ({ isSynced, } = useMUD(); - const [armor, setArmor] = useState([]); - const [weapons, setWeapons] = useState([]); + const [armorTemplates, setArmorTemplates] = useState([]); + const [weaponTemplates, setWeaponTemplates] = useState([]); const [isLoading, setIsLoading] = useState(true); const fetchAllArmor = useCallback( @@ -194,14 +194,14 @@ export const ItemsProvider = ({ .map(({ tokenId }) => tokenId); const _armor = await fetchAllArmor(allArmorIds); - setArmor(_armor); + setArmorTemplates(_armor); const allWeaponIds = allItemIds .filter(({ itemType }) => itemType === ItemType.Weapon) .map(({ tokenId }) => tokenId); const _weapons = await fetchAllWeapons(allWeaponIds); - setWeapons(_weapons); + setWeaponTemplates(_weapons); } } catch (e) { renderError((e as Error)?.message ?? 'Failed to fetch items.', e); @@ -214,8 +214,8 @@ export const ItemsProvider = ({ return ( diff --git a/packages/client/src/pages/Character.tsx b/packages/client/src/pages/Character.tsx index 5777c1944..4fbda9ef3 100644 --- a/packages/client/src/pages/Character.tsx +++ b/packages/client/src/pages/Character.tsx @@ -20,14 +20,8 @@ import { Entity, getComponentValue, getComponentValueStrict, - Has, - runQuery, } from '@latticexyz/recs'; -import { - decodeEntity, - encodeEntity, - singletonEntity, -} from '@latticexyz/store-sync/recs'; +import { encodeEntity } from '@latticexyz/store-sync/recs'; import { useCallback, useEffect, useMemo, useState } from 'react'; import { FaHatWizard } from 'react-icons/fa'; import { GiAxeSword, GiRogue } from 'react-icons/gi'; @@ -41,21 +35,19 @@ import { ItemEquipModal } from '../components/ItemEquipModal'; import { Level } from '../components/Level'; import { LevelingPanel } from '../components/LevelingPanel'; import { useCharacter } from '../contexts/CharacterContext'; +import { useItems } from '../contexts/ItemsContext'; import { useMUD } from '../contexts/MUDContext'; import { useToast } from '../hooks/useToast'; import { AUCTION_HOUSE_PATH, HOME_PATH, LEADERBOARD_PATH } from '../Routes'; import { MAX_EQUIPPED_ARMOR, MAX_EQUIPPED_WEAPONS } from '../utils/constants'; import { - decodeArmorStats, decodeCharacterId, - decodeWeaponStats, fetchMetadataFromUri, uriToHttp, } from '../utils/helpers'; import { type Armor, type Character, - ItemType, StatsClasses, type Weapon, } from '../utils/types'; @@ -445,14 +437,13 @@ const ItemsPanel = ({ character }: { character: Character }): JSX.Element => { const { renderError } = useToast(); const { - components: { - CharacterEquipment, - Items, - ItemsBaseURI, - ItemsOwners, - ItemsTokenURI, - }, + components: { CharacterEquipment, ItemsOwners }, } = useMUD(); + const { + armorTemplates, + isLoading: isLoadingItemTemplates, + weaponTemplates, + } = useItems(); const { isOpen: isItemModalOpen, @@ -476,129 +467,52 @@ const ItemsPanel = ({ character }: { character: Character }): JSX.Element => { const maxWeaponsEquipped = equippedWeapons.length === MAX_EQUIPPED_WEAPONS; const fetchCharacterItems = useCallback( - async (_character: Character) => { + (_character: Character) => { try { - const _items = Array.from(runQuery([Has(ItemsOwners)])) - .map(entity => { - const itemdBalance = getComponentValueStrict( - ItemsOwners, - entity, - ).balance; - - const { owner, tokenId } = decodeEntity( + const _armor = armorTemplates + .map(armor => { + const tokenOwnersEntity = encodeEntity( { owner: 'address', tokenId: 'uint256' }, - entity, - ); - - const tokenIdEntity = encodeEntity( - { tokenId: 'uint256' }, - { tokenId }, + { + owner: _character.owner as `0x${string}`, + tokenId: BigInt(armor.tokenId), + }, ); - const itemTemplate = getComponentValueStrict(Items, tokenIdEntity); + const itemOwner = getComponentValue(ItemsOwners, tokenOwnersEntity); return { - balance: itemdBalance.toString(), - itemId: entity, - itemType: itemTemplate.itemType, - owner, - stats: itemTemplate.stats, - tokenId: tokenId.toString(), - tokenIdEntity, - }; + ...armor, + balance: itemOwner ? itemOwner.balance.toString() : '0', + itemId: tokenOwnersEntity, + owner: _character.owner, + } as Armor; }) - .filter(item => item.owner === _character.owner) - .sort((a, b) => { - return Number(a.tokenId) - Number(b.tokenId); - }); - - const _armor = _items.filter(item => item.itemType === ItemType.Armor); - const _weapons = _items.filter( - item => item.itemType === ItemType.Weapon, - ); - - const fullArmor = await Promise.all( - _armor.map(async item => { - const decodedArmorStats = decodeArmorStats(item.stats); - - const baseURI = getComponentValueStrict( - ItemsBaseURI, - singletonEntity, - ).uri; - - const tokenURI = getComponentValueStrict( - ItemsTokenURI, - item.tokenIdEntity, - ).uri; - - const metadata = await fetchMetadataFromUri( - uriToHttp(`${baseURI}${tokenURI}`)[0], - ); + .filter(a => a.balance !== '0'); - return { - ...metadata, - agiModifier: decodedArmorStats.agiModifier, - armorModifier: decodedArmorStats.armorModifier, - balance: item.balance, - hitPointModifier: decodedArmorStats.hitPointModifier, - intModifier: decodedArmorStats.intModifier, - itemId: item.itemId, - owner: item.owner, - statRestrictions: { - minAgility: decodedArmorStats.statRestrictions.minAgility, - minIntelligence: - decodedArmorStats.statRestrictions.minIntelligence, - minStrength: decodedArmorStats.statRestrictions.minStrength, + const _weapons = weaponTemplates + .map(weapon => { + const tokenOwnersEntity = encodeEntity( + { owner: 'address', tokenId: 'uint256' }, + { + owner: _character.owner as `0x${string}`, + tokenId: BigInt(weapon.tokenId), }, - strModifier: decodedArmorStats.strModifier, - tokenId: item.tokenId, - } as Armor; - }), - ); - - const fullWeapons = await Promise.all( - _weapons.map(async item => { - const decodedWeaponStats = decodeWeaponStats(item.stats); - - const baseURI = getComponentValueStrict( - ItemsBaseURI, - singletonEntity, - ).uri; - - const tokenURI = getComponentValueStrict( - ItemsTokenURI, - item.tokenIdEntity, - ).uri; - - const metadata = await fetchMetadataFromUri( - uriToHttp(`${baseURI}${tokenURI}`)[0], ); + const itemOwner = getComponentValue(ItemsOwners, tokenOwnersEntity); + return { - ...metadata, - agiModifier: decodedWeaponStats.agiModifier, - balance: item.balance, - hitPointModifier: decodedWeaponStats.hitPointModifier, - intModifier: decodedWeaponStats.intModifier, - itemId: item.itemId, - maxDamage: decodedWeaponStats.maxDamage, - minDamage: decodedWeaponStats.minDamage, - minLevel: decodedWeaponStats.minLevel, - owner: item.owner, - statRestrictions: { - minAgility: decodedWeaponStats.statRestrictions.minAgility, - minIntelligence: - decodedWeaponStats.statRestrictions.minIntelligence, - minStrength: decodedWeaponStats.statRestrictions.minStrength, - }, - strModifier: decodedWeaponStats.strModifier, - tokenId: item.tokenId, + ...weapon, + balance: itemOwner ? itemOwner.balance.toString() : '0', + itemId: tokenOwnersEntity, + owner: _character.owner, } as Weapon; - }), - ); + }) + .filter(w => w.balance !== '0'); - setArmor(fullArmor); - setWeapons(fullWeapons); + setArmor(_armor); + setWeapons(_weapons); } catch (e) { renderError( (e as Error)?.message ?? 'Failed to fetch character items.', @@ -608,14 +522,13 @@ const ItemsPanel = ({ character }: { character: Character }): JSX.Element => { setIsLoadingItems(false); } }, - [Items, ItemsBaseURI, ItemsOwners, ItemsTokenURI, renderError], + [armorTemplates, ItemsOwners, renderError, weaponTemplates], ); useEffect(() => { - (async (): Promise => { - await fetchCharacterItems(character); - })(); - }, [character, fetchCharacterItems]); + if (isLoadingItemTemplates) return; + fetchCharacterItems(character); + }, [character, fetchCharacterItems, isLoadingItemTemplates]); if (isLoadingItems) { return ( diff --git a/packages/client/src/pages/CharacterCreation.tsx b/packages/client/src/pages/CharacterCreation.tsx index 065864109..9c5a26a6b 100644 --- a/packages/client/src/pages/CharacterCreation.tsx +++ b/packages/client/src/pages/CharacterCreation.tsx @@ -18,7 +18,7 @@ import { } from '@chakra-ui/react'; import { useComponentValue } from '@latticexyz/react'; import { getComponentValueStrict, Has, runQuery } from '@latticexyz/recs'; -import { encodeEntity, singletonEntity } from '@latticexyz/store-sync/recs'; +import { singletonEntity } from '@latticexyz/store-sync/recs'; import { useCallback, useEffect, useMemo, useState } from 'react'; import { FaLock } from 'react-icons/fa'; import { useNavigate } from 'react-router-dom'; @@ -26,18 +26,13 @@ import { useAccount } from 'wagmi'; import { ItemCardSmall } from '../components/ItemCard'; import { useCharacter } from '../contexts/CharacterContext'; +import { useItems } from '../contexts/ItemsContext'; import { useMUD } from '../contexts/MUDContext'; import { useToast } from '../hooks/useToast'; import { useUploadFile } from '../hooks/useUploadFile'; import { GAME_BOARD_PATH, HOME_PATH } from '../Routes'; import { API_URL } from '../utils/constants'; -import { - decodeArmorStats, - decodeWeaponStats, - fetchMetadataFromUri, - shortenAddress, - uriToHttp, -} from '../utils/helpers'; +import { shortenAddress } from '../utils/helpers'; import { type Armor, StatsClasses, type Weapon } from '../utils/types'; export const CharacterCreation = (): JSX.Element => { @@ -46,17 +41,16 @@ export const CharacterCreation = (): JSX.Element => { const isSmallScreen = useBreakpointValue({ base: true, lg: false }); const { isConnected } = useAccount(); const { - components: { - Items, - ItemsBaseURI, - ItemsTokenURI, - StarterItems, - UltimateDominionConfig, - }, + components: { Items, StarterItems, UltimateDominionConfig }, delegatorAddress, isSynced, systemCalls: { enterGame, mintCharacter, rollStats }, } = useMUD(); + const { + armorTemplates, + isLoading: isLoadingItemTemplates, + weaponTemplates, + } = useItems(); const { character, isRefreshing, refreshCharacter } = useCharacter(); const { file: avatar, @@ -88,103 +82,9 @@ export const CharacterCreation = (): JSX.Element => { setShowError(false); }, [avatar, description, name]); - const fetchStarterItems = useCallback( - async (starterArmorTokenIds: bigint[], starterWeaponTokenIds: bigint[]) => { - try { - const _armor: Armor[] = await Promise.all( - starterArmorTokenIds.map(async tokenId => { - const tokenIdEntity = encodeEntity( - { tokenId: 'uint256' }, - { tokenId }, - ); - - const itemTemplate = getComponentValueStrict(Items, tokenIdEntity); - const decodedArmorStats = decodeArmorStats(itemTemplate.stats); - - const baseURI = getComponentValueStrict( - ItemsBaseURI, - singletonEntity, - ).uri; - - const tokenURI = getComponentValueStrict( - ItemsTokenURI, - tokenIdEntity, - ).uri; - - const fetachedMetadata = await fetchMetadataFromUri( - uriToHttp(`${baseURI}${tokenURI}`)[0], - ); - - return { - agiModifier: decodedArmorStats.agiModifier, - armorModifier: decodedArmorStats.armorModifier, - hitPointModifier: decodedArmorStats.hitPointModifier, - intModifier: decodedArmorStats.intModifier, - statRestrictions: { - minAgility: decodedArmorStats.statRestrictions.minAgility, - minIntelligence: - decodedArmorStats.statRestrictions.minIntelligence, - minStrength: decodedArmorStats.statRestrictions.minStrength, - }, - strModifier: decodedArmorStats.strModifier, - ...fetachedMetadata, - } as Armor; - }), - ); - - const _weapons: Weapon[] = await Promise.all( - starterWeaponTokenIds.map(async tokenId => { - const tokenIdEntity = encodeEntity( - { tokenId: 'uint256' }, - { tokenId }, - ); - - const itemTemplate = getComponentValueStrict(Items, tokenIdEntity); - const decodedWeaponStats = decodeWeaponStats(itemTemplate.stats); - - const baseURI = getComponentValueStrict( - ItemsBaseURI, - singletonEntity, - ).uri; - - const tokenURI = getComponentValueStrict( - ItemsTokenURI, - tokenIdEntity, - ).uri; - - const fetachedMetadata = await fetchMetadataFromUri( - uriToHttp(`${baseURI}${tokenURI}`)[0], - ); - - return { - agiModifier: decodedWeaponStats.agiModifier, - hitPointModifier: decodedWeaponStats.hitPointModifier, - intModifier: decodedWeaponStats.intModifier, - maxDamage: decodedWeaponStats.maxDamage, - minDamage: decodedWeaponStats.minDamage, - minLevel: decodedWeaponStats.minLevel, - statRestrictions: { - minAgility: decodedWeaponStats.statRestrictions.minAgility, - minIntelligence: - decodedWeaponStats.statRestrictions.minIntelligence, - minStrength: decodedWeaponStats.statRestrictions.minStrength, - }, - strModifier: decodedWeaponStats.strModifier, - ...fetachedMetadata, - } as Weapon; - }), - ); - - setStarterArmor(_armor); - setStarterWeapons(_weapons); - } catch (e) { - renderError((e as Error)?.message ?? 'Error fetching starter item.', e); - } - }, - [Items, ItemsBaseURI, ItemsTokenURI, renderError], - ); - useEffect(() => { + if (isLoadingItemTemplates) return; + const starterItemTokenIds = Array.from(runQuery([Has(StarterItems)])).map( entity => { const tokenIds = getComponentValueStrict(StarterItems, entity).itemIds; @@ -193,15 +93,28 @@ export const CharacterCreation = (): JSX.Element => { ); const starterArmorTokenIds = starterItemTokenIds - .map(item => item[0] as bigint) + .map(item => item[0].toString()) .filter((value, index, self) => self.indexOf(value) === index); + const starterWeaponTokenIds = starterItemTokenIds.map(item => + item[1].toString(), + ); - const starterWeaponTokenIds = starterItemTokenIds.map( - item => item[1] as bigint, + const _starterArmor = armorTemplates.filter(armor => + starterArmorTokenIds.includes(armor.tokenId), + ); + const _starterWeapons = weaponTemplates.filter(weapon => + starterWeaponTokenIds.includes(weapon.tokenId), ); - fetchStarterItems(starterArmorTokenIds, starterWeaponTokenIds); - }, [fetchStarterItems, Items, StarterItems]); + setStarterArmor(_starterArmor); + setStarterWeapons(_starterWeapons); + }, [ + armorTemplates, + isLoadingItemTemplates, + Items, + StarterItems, + weaponTemplates, + ]); const onUploadAvatar = useCallback(() => { const input = document.getElementById('avatarInput');