From 7ec56f76a74131a61d37e5214c7e6c35a06e3533 Mon Sep 17 00:00:00 2001 From: ECWireless Date: Tue, 9 Jul 2024 10:55:35 -0600 Subject: [PATCH 1/2] Create a MapNavigationContext --- .../src/components/TileDetailsPanel.tsx | 232 +------------- .../src/contexts/MapNavigationContext.tsx | 292 ++++++++++++++++++ packages/client/src/index.tsx | 5 +- 3 files changed, 306 insertions(+), 223 deletions(-) create mode 100644 packages/client/src/contexts/MapNavigationContext.tsx diff --git a/packages/client/src/components/TileDetailsPanel.tsx b/packages/client/src/components/TileDetailsPanel.tsx index b3696be06..4f30eb00b 100644 --- a/packages/client/src/components/TileDetailsPanel.tsx +++ b/packages/client/src/components/TileDetailsPanel.tsx @@ -5,239 +5,27 @@ import { Grid, GridItem, HStack, + Spinner, Text, useBreakpointValue, } from '@chakra-ui/react'; -import { useComponentValue, useEntityQuery } from '@latticexyz/react'; -import { - Entity, - getComponentValue, - getComponentValueStrict, - Has, - HasValue, -} from '@latticexyz/recs'; -import { encodeEntity } from '@latticexyz/store-sync/recs'; -import { useCallback, useEffect, useState } from 'react'; import { IoIosArrowForward } from 'react-icons/io'; -import { - bytesToHex, - formatEther, - getContract, - hexToBytes, - hexToString, -} from 'viem'; -import { useCharacter } from '../contexts/CharacterContext'; -import { useMUD } from '../contexts/MUDContext'; -import { fetchMetadataFromUri, uriToHttp } from '../utils/helpers'; +import { useMapNavigation } from '../contexts/MapNavigationContext'; import { type Character, type Monster } from '../utils/types'; const ROW_HEIGHT = { base: 5, md: 8, lg: 10 }; export const TileDetailsPanel = (): JSX.Element => { - const { - components: { Characters, Mobs, Position, Spawned, Stats }, - delegatorAddress, - network: { publicClient, worldContract }, - } = useMUD(); - const { character } = useCharacter(); - - const [otherPlayers, setOtherPlayers] = useState([]); - const [monsters, setMonsters] = useState([]); - - const characterPosition = useComponentValue( - Position, - encodeEntity( - { characterId: 'uint256' }, - { characterId: BigInt(character?.characterId ?? 0) }, - ), - ); - - const allEntities = useEntityQuery([ - Has(Spawned), - HasValue(Position, { - x: characterPosition?.x, - y: characterPosition?.y, - }), - ]); - - const getOtherCharacters = useCallback( - async (entities: Entity[]): Promise => { - if (!(delegatorAddress && publicClient && worldContract)) return; - - const characters: Character[] = await Promise.all( - entities.map(async (entity: Entity) => { - const characterData = getComponentValueStrict(Characters, entity); - const characterStats = getComponentValueStrict(Stats, entity); - - const entityBytes = hexToBytes(entity.toString() as `0x${string}`); - const tokenBytes = entityBytes.slice(20); - const tokenId = BigInt(bytesToHex(tokenBytes)).toString(); - - const characterTokenAddress = - await worldContract.read.UD__getCharacterToken(); - - const characterToken = getContract({ - address: characterTokenAddress, - abi: [ - { - type: 'function', - name: 'tokenURI', - inputs: [ - { - name: 'tokenId', - type: 'uint256', - internalType: 'uint256', - }, - ], - outputs: [ - { - name: '', - type: 'string', - internalType: 'string', - }, - ], - stateMutability: 'view', - }, - ], - client: publicClient, - }); - - const metadataURI = await characterToken.read.tokenURI([ - BigInt(tokenId), - ]); - - const fetachedMetadata = await fetchMetadataFromUri( - uriToHttp(metadataURI)[0], - ); - - const goldTokenAddress = await worldContract.read.UD__getGoldToken(); - - const goldToken = getContract({ - address: goldTokenAddress, - abi: [ - { - type: 'function', - name: 'balanceOf', - inputs: [ - { - name: 'account', - type: 'address', - internalType: 'address', - }, - ], - outputs: [ - { - name: '', - type: 'uint256', - internalType: 'uint256', - }, - ], - stateMutability: 'view', - }, - ], - client: publicClient, - }); - - const goldBalance = await goldToken.read.balanceOf([ - delegatorAddress, - ]); + const { isRefreshing, monsters, otherPlayers } = useMapNavigation(); - return { - ...fetachedMetadata, - agility: characterStats?.agility.toString() ?? '0', - characterClass: characterData.class, - characterId: entity, - goldBalance: formatEther(BigInt(goldBalance)).toString(), - experience: characterStats?.experience.toString() ?? '0', - intelligence: characterStats?.intelligence.toString() ?? '0', - maxHitPoints: characterStats?.maxHitPoints.toString() ?? '0', - level: characterStats?.level.toString() ?? '0', - locked: characterData.locked, - name: hexToString(characterData.name as `0x${string}`, { - size: 32, - }), - owner: characterData.owner, - strength: characterStats?.strength.toString() ?? '0', - tokenId, - } as Character; - }), - ); - - setOtherPlayers(characters.filter(c => c.owner !== delegatorAddress)); - }, - [Characters, Stats, delegatorAddress, publicClient, worldContract], - ); - - const getMonsters = useCallback( - async (entities: Entity[]): Promise => { - const monsterAndMobIds = entities.map(entity => { - const entityBytes = hexToBytes(entity.toString() as `0x${string}`); - const mobIdBytes = entityBytes.slice(0, 4); - return { - mobId: BigInt(bytesToHex(mobIdBytes)).toString(), - monsterId: entity, - }; - }); - - const _monsters: Monster[] = await Promise.all( - monsterAndMobIds.map(async monsterAndMobId => { - const { monsterId, mobId } = monsterAndMobId; - const mobData = getComponentValueStrict( - Mobs, - encodeEntity({ mobId: 'uint256' }, { mobId: BigInt(mobId) }), - ); - const monsterStats = getComponentValueStrict(Stats, monsterId); - - const { mobMetadata: metadataURI } = mobData; - - const monsterTemplateStats = - (await worldContract.read.UD__getMonsterStats([ - monsterId as `0x${string}`, - ])) as { class: number }; - - const fetachedMetadata = await fetchMetadataFromUri( - uriToHttp(metadataURI)[0], - ); - - return { - class: monsterTemplateStats.class, - level: monsterStats.level.toString(), - mobId, - monsterId, - ...fetachedMetadata, - }; - }), - ); - - setMonsters(_monsters); - }, - [Mobs, Stats, worldContract], - ); - - useEffect(() => { - (async (): Promise => { - if (!allEntities) return; - - const characterEntities: Entity[] = []; - const monsterEntities: Entity[] = []; - - await Promise.all( - allEntities.map(async entity => { - const characterData = getComponentValue(Characters, entity); - - if (characterData) { - characterEntities.push(entity); - } else { - monsterEntities.push(entity); - } - }), - ); - - await getOtherCharacters(characterEntities); - await getMonsters(monsterEntities); - })(); - }, [allEntities, Characters, getMonsters, getOtherCharacters]); + if (isRefreshing) { + return ( + + + + ); + } return ( diff --git a/packages/client/src/contexts/MapNavigationContext.tsx b/packages/client/src/contexts/MapNavigationContext.tsx new file mode 100644 index 000000000..6ff0f542a --- /dev/null +++ b/packages/client/src/contexts/MapNavigationContext.tsx @@ -0,0 +1,292 @@ +import { useComponentValue, useEntityQuery } from '@latticexyz/react'; +import { + Entity, + getComponentValue, + getComponentValueStrict, + Has, + HasValue, +} from '@latticexyz/recs'; +import { encodeEntity } from '@latticexyz/store-sync/recs'; +import { + createContext, + ReactNode, + useCallback, + useContext, + useEffect, + useState, +} from 'react'; +import { + bytesToHex, + formatEther, + getContract, + hexToBytes, + hexToString, +} from 'viem'; + +import { useToast } from '../hooks/useToast'; +import { fetchMetadataFromUri, uriToHttp } from '../utils/helpers'; +import type { Character, Monster } from '../utils/types'; +import { useCharacter } from './CharacterContext'; +import { useMUD } from './MUDContext'; + +type MapNavigationContextType = { + isRefreshing: boolean; + monsters: Monster[]; + otherPlayers: Character[]; +}; + +const MapNavigationContext = createContext({ + isRefreshing: false, + monsters: [], + otherPlayers: [], +}); + +export type NavigationProviderProps = { + children: ReactNode; +}; + +export const MapNavigationProvider = ({ + children, +}: NavigationProviderProps): JSX.Element => { + const { + components: { Characters, Mobs, Position, Spawned, Stats }, + delegatorAddress, + network: { publicClient, worldContract }, + } = useMUD(); + const { renderError } = useToast(); + const { character } = useCharacter(); + + const [otherPlayers, setOtherPlayers] = useState([]); + const [monsters, setMonsters] = useState([]); + + const [isRefreshing, setIsRefreshing] = useState(false); + + const characterPosition = useComponentValue( + Position, + encodeEntity( + { characterId: 'uint256' }, + { characterId: BigInt(character?.characterId ?? 0) }, + ), + ); + + const allEntities = useEntityQuery([ + Has(Spawned), + HasValue(Position, { + x: characterPosition?.x, + y: characterPosition?.y, + }), + ]); + + const getOtherCharacters = useCallback( + async (entities: Entity[]): Promise => { + if (!(delegatorAddress && publicClient && worldContract)) return; + + try { + const characters: Character[] = await Promise.all( + entities.map(async (entity: Entity) => { + const characterData = getComponentValueStrict(Characters, entity); + const characterStats = getComponentValueStrict(Stats, entity); + + const entityBytes = hexToBytes(entity.toString() as `0x${string}`); + const tokenBytes = entityBytes.slice(20); + const tokenId = BigInt(bytesToHex(tokenBytes)).toString(); + + const characterTokenAddress = + await worldContract.read.UD__getCharacterToken(); + + const characterToken = getContract({ + address: characterTokenAddress, + abi: [ + { + type: 'function', + name: 'tokenURI', + inputs: [ + { + name: 'tokenId', + type: 'uint256', + internalType: 'uint256', + }, + ], + outputs: [ + { + name: '', + type: 'string', + internalType: 'string', + }, + ], + stateMutability: 'view', + }, + ], + client: publicClient, + }); + + const metadataURI = await characterToken.read.tokenURI([ + BigInt(tokenId), + ]); + + const fetachedMetadata = await fetchMetadataFromUri( + uriToHttp(metadataURI)[0], + ); + + const goldTokenAddress = + await worldContract.read.UD__getGoldToken(); + + const goldToken = getContract({ + address: goldTokenAddress, + abi: [ + { + type: 'function', + name: 'balanceOf', + inputs: [ + { + name: 'account', + type: 'address', + internalType: 'address', + }, + ], + outputs: [ + { + name: '', + type: 'uint256', + internalType: 'uint256', + }, + ], + stateMutability: 'view', + }, + ], + client: publicClient, + }); + + const goldBalance = await goldToken.read.balanceOf([ + delegatorAddress, + ]); + + return { + ...fetachedMetadata, + agility: characterStats?.agility.toString() ?? '0', + characterClass: characterData.class, + characterId: entity, + goldBalance: formatEther(BigInt(goldBalance)).toString(), + experience: characterStats?.experience.toString() ?? '0', + intelligence: characterStats?.intelligence.toString() ?? '0', + maxHitPoints: characterStats?.maxHitPoints.toString() ?? '0', + level: characterStats?.level.toString() ?? '0', + locked: characterData.locked, + name: hexToString(characterData.name as `0x${string}`, { + size: 32, + }), + owner: characterData.owner, + strength: characterStats?.strength.toString() ?? '0', + tokenId, + } as Character; + }), + ); + + setOtherPlayers(characters.filter(c => c.owner !== delegatorAddress)); + } catch (error) { + renderError(error, 'Failed to fetch other players'); + } + }, + [ + Characters, + Stats, + delegatorAddress, + publicClient, + renderError, + worldContract, + ], + ); + + const getMonsters = useCallback( + async (entities: Entity[]): Promise => { + try { + const monsterAndMobIds = entities.map(entity => { + const entityBytes = hexToBytes(entity.toString() as `0x${string}`); + const mobIdBytes = entityBytes.slice(0, 4); + return { + mobId: BigInt(bytesToHex(mobIdBytes)).toString(), + monsterId: entity, + }; + }); + + const _monsters: Monster[] = await Promise.all( + monsterAndMobIds.map(async monsterAndMobId => { + const { monsterId, mobId } = monsterAndMobId; + const mobData = getComponentValueStrict( + Mobs, + encodeEntity({ mobId: 'uint256' }, { mobId: BigInt(mobId) }), + ); + const monsterStats = getComponentValueStrict(Stats, monsterId); + + const { mobMetadata: metadataURI } = mobData; + + const monsterTemplateStats = + (await worldContract.read.UD__getMonsterStats([ + monsterId as `0x${string}`, + ])) as { class: number }; + + const fetachedMetadata = await fetchMetadataFromUri( + uriToHttp(metadataURI)[0], + ); + + return { + class: monsterTemplateStats.class, + level: monsterStats.level.toString(), + mobId, + monsterId, + ...fetachedMetadata, + }; + }), + ); + + setMonsters(_monsters); + } catch (error) { + renderError(error, 'Failed to fetch monsters'); + } + }, + [Mobs, renderError, Stats, worldContract], + ); + + useEffect(() => { + (async (): Promise => { + if (!allEntities) return; + + setIsRefreshing(true); + + const characterEntities: Entity[] = []; + const monsterEntities: Entity[] = []; + + await Promise.all( + allEntities.map(async entity => { + const characterData = getComponentValue(Characters, entity); + + if (characterData) { + characterEntities.push(entity); + } else { + monsterEntities.push(entity); + } + }), + ); + + await getOtherCharacters(characterEntities); + await getMonsters(monsterEntities); + + setIsRefreshing(false); + })(); + }, [allEntities, Characters, getMonsters, getOtherCharacters]); + + return ( + + {children} + + ); +}; + +export const useMapNavigation = (): MapNavigationContextType => + useContext(MapNavigationContext); diff --git a/packages/client/src/index.tsx b/packages/client/src/index.tsx index e4ea6f209..47601b3fa 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 { MapNavigationProvider } from './contexts/MapNavigationContext'; import { MUDProvider } from './contexts/MUDContext'; import { Web3Provider } from './contexts/Web3Provider'; import { setup } from './lib/mud/setup'; @@ -31,7 +32,9 @@ setup().then(async result => { - + + + {import.meta.env.DEV && } From b2966fd53c067f428314966434c718f6e3420e2a Mon Sep 17 00:00:00 2001 From: ECWireless Date: Wed, 10 Jul 2024 08:07:30 -0600 Subject: [PATCH 2/2] Put move and spawn functions in map nav context --- packages/client/src/components/MapPanel.tsx | 139 +---------------- .../src/contexts/MapNavigationContext.tsx | 144 +++++++++++++++++- 2 files changed, 140 insertions(+), 143 deletions(-) diff --git a/packages/client/src/components/MapPanel.tsx b/packages/client/src/components/MapPanel.tsx index 513a4e445..8a60af12e 100644 --- a/packages/client/src/components/MapPanel.tsx +++ b/packages/client/src/components/MapPanel.tsx @@ -1,7 +1,4 @@ import { Box, Button, HStack, Stack, Text, VStack } from '@chakra-ui/react'; -import { useComponentValue } from '@latticexyz/react'; -import { encodeEntity } from '@latticexyz/store-sync/recs'; -import { useCallback, useState } from 'react'; import { BiSolidNavigation } from 'react-icons/bi'; import { IoIosArrowDropdownCircle, @@ -11,9 +8,7 @@ import { } from 'react-icons/io'; import { TbDirectionArrows } from 'react-icons/tb'; -import { useCharacter } from '../contexts/CharacterContext'; -import { useMUD } from '../contexts/MUDContext'; -import { useToast } from '../hooks/useToast'; +import { useMapNavigation } from '../contexts/MapNavigationContext'; const SAFE_ZONE_AREA = { topLeft: { x: 0, y: 4 }, @@ -21,134 +16,8 @@ const SAFE_ZONE_AREA = { }; export const MapPanel = (): JSX.Element => { - const { renderError, renderSuccess } = useToast(); - const { - burnerBalance, - components: { Position, Spawned }, - delegatorAddress, - systemCalls: { move, spawn }, - } = useMUD(); - const { character } = useCharacter(); - - const [isSpawning, setIsSpawning] = useState(false); - const [isMoving, setIsMoving] = useState(false); - - const position = useComponentValue( - Position, - encodeEntity( - { characterId: 'uint256' }, - { characterId: BigInt(character?.characterId ?? 0) }, - ), - ); - - const isSpawned = !!useComponentValue( - Spawned, - encodeEntity( - { characterId: 'uint256' }, - { characterId: BigInt(character?.characterId ?? 0) }, - ), - )?.spawned; - - const onSpawn = useCallback(async () => { - try { - setIsSpawning(true); - - if (burnerBalance === '0') { - throw new Error( - 'Insufficient funds. Please top off your session account.', - ); - } - - if (!delegatorAddress) { - throw new Error('Missing delegation.'); - } - - if (!character) { - throw new Error('Character not found.'); - } - - const success = await spawn(character.characterId); - - if (!success) { - throw new Error('Contract call failed'); - } - - renderSuccess('Spawned!'); - } catch (e) { - renderError(e, 'Failed to roll stats.'); - } finally { - setIsSpawning(false); - } - }, [ - burnerBalance, - character, - delegatorAddress, - renderError, - renderSuccess, - spawn, - ]); - - const onMove = useCallback( - async (direction: 'up' | 'down' | 'left' | 'right') => { - try { - setIsMoving(true); - - if (!delegatorAddress) { - throw new Error('Burner not found'); - } - - if (!position) { - throw new Error('Position not found'); - } - - if (!character) { - throw new Error('Character not found'); - } - - const { x, y } = position; - - if ( - (direction === 'up' && position.y === 9) || - (direction === 'down' && position.y === 0) || - (direction === 'left' && position.x === 0) || - (direction === 'right' && position.x === 9) - ) { - return; - } - - let newX = x; - let newY = y; - - switch (direction) { - case 'up': - newY = y + 1; - break; - case 'down': - newY = y - 1; - break; - case 'left': - newX = x - 1; - break; - case 'right': - newX = x + 1; - break; - default: - break; - } - - const success = await move(character.characterId, newX, newY); - - if (!success) { - throw new Error('Contract call failed'); - } - } catch (e) { - renderError(e, 'Failed to move.'); - } finally { - setIsMoving(false); - } - }, - [character, delegatorAddress, move, position, renderError], - ); + const { isRefreshing, isSpawned, isSpawning, onMove, onSpawn, position } = + useMapNavigation(); return ( { {isSpawned ? ( - + ) : (