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 ? (
-
+
) : (