From 94abe6c2f30092ff7833ee427488ac5c533984e8 Mon Sep 17 00:00:00 2001
From: ECWireless
Date: Sun, 14 Jul 2024 11:02:48 -0600
Subject: [PATCH 1/7] Render items to character page
---
.../components/Character/Card/ItemCard.tsx | 57 +---
.../src/contexts/MapNavigationContext.tsx | 15 +-
packages/client/src/lib/mud/externalTables.ts | 24 +-
packages/client/src/pages/Character.tsx | 279 +++++++++---------
.../client/src/pages/CharacterCreation.tsx | 65 ++--
packages/client/src/utils/types.ts | 9 +-
6 files changed, 214 insertions(+), 235 deletions(-)
diff --git a/packages/client/src/components/Character/Card/ItemCard.tsx b/packages/client/src/components/Character/Card/ItemCard.tsx
index 041dacc90..5589221be 100644
--- a/packages/client/src/components/Character/Card/ItemCard.tsx
+++ b/packages/client/src/components/Character/Card/ItemCard.tsx
@@ -6,38 +6,15 @@ import {
Center,
Text,
} from '@chakra-ui/react';
-import {
- FaBook,
- FaBug,
- FaDatabase,
- FaDoorClosed,
- FaFire,
- FaPizzaSlice,
- FaRoad,
- FaScribd,
- FaSearchLocation,
- FaShieldAlt,
- FaSocks,
- FaStarAndCrescent,
-} from 'react-icons/fa';
+import { GiRogue } from 'react-icons/gi';
+
+import type { Weapon } from '../../../utils/types';
+
+export const ItemCard = (weapon: Weapon): JSX.Element => {
+ const { agiModifier, intModifier, strModifier, name } = weapon;
+
+ const disabled = false;
-export const ItemCard = ({
- agi,
- disabled,
- icon,
- image,
- int,
- name,
- str,
-}: {
- agi: number;
- disabled: boolean;
- icon: string;
- image: string;
- int: number;
- name: string;
- str: number;
-}): JSX.Element => {
return (
- {image == 'book' && }
- {image == 'bug' && }
- {image == 'database' && }
- {image == 'door-closed' && }
- {image == 'pizza-slice' && }
- {image == 'scribd' && }
- {image == 'search' && }
- {image == 'socks' && }
- {image == 'star-crescent' && }
+ {name.slice(-3)}
- {name}
+ {name.slice(0, -3)}
- STR+{str} AGI+{agi} INT+{int}
+ STR+{strModifier} AGI+{agiModifier} INT+{intModifier}
- {icon == 'fire' && }
- {icon == 'road' && }
- {icon == 'shield' && }
+
diff --git a/packages/client/src/contexts/MapNavigationContext.tsx b/packages/client/src/contexts/MapNavigationContext.tsx
index ac948bd38..522cb64ec 100644
--- a/packages/client/src/contexts/MapNavigationContext.tsx
+++ b/packages/client/src/contexts/MapNavigationContext.tsx
@@ -4,6 +4,7 @@ import {
getComponentValueStrict,
Has,
HasValue,
+ Not,
} from '@latticexyz/recs';
import { encodeEntity } from '@latticexyz/store-sync/recs';
import {
@@ -95,9 +96,10 @@ export const MapNavigationProvider = ({
),
)?.spawned;
- const allEntities = useEntityQuery([
+ const allMonsterEntities = useEntityQuery([
Has(Spawned),
Has(Stats),
+ Not(Characters),
HasValue(Position, {
x: position?.x,
y: position?.y,
@@ -106,6 +108,7 @@ export const MapNavigationProvider = ({
const allCharacterEntities = useEntityQuery([
Has(Characters),
+ Has(Spawned),
Has(Stats),
HasValue(Position, {
x: position?.x,
@@ -235,13 +238,7 @@ export const MapNavigationProvider = ({
useEffect(() => {
(async (): Promise => {
- if (!(allCharacterEntities && allEntities)) return;
-
- setIsFetchingEntities(true);
-
- const allMonsterEntities = allEntities.filter(
- entity => !allCharacterEntities.includes(entity),
- );
+ if (!(allCharacterEntities && allMonsterEntities)) return;
await getOtherCharacters(allCharacterEntities);
await getMonsters(allMonsterEntities);
@@ -250,7 +247,7 @@ export const MapNavigationProvider = ({
})();
}, [
allCharacterEntities,
- allEntities,
+ allMonsterEntities,
Characters,
getMonsters,
getOtherCharacters,
diff --git a/packages/client/src/lib/mud/externalTables.ts b/packages/client/src/lib/mud/externalTables.ts
index b35156aa6..339da4062 100644
--- a/packages/client/src/lib/mud/externalTables.ts
+++ b/packages/client/src/lib/mud/externalTables.ts
@@ -9,6 +9,7 @@ const CharactersBalancesTableId = resourceToHex({
namespace: CHARACTERS_NAMESPACE,
name: 'Balances',
});
+
const CharactersTokenURITableId = resourceToHex({
type: 'table',
namespace: CHARACTERS_NAMESPACE,
@@ -21,6 +22,12 @@ const GoldBalancesTableId = resourceToHex({
name: 'Balances',
});
+const ItemsBaseURITableId = resourceToHex({
+ type: 'table',
+ namespace: ITEMS_NAMESPACE,
+ name: 'MetadataURI',
+});
+
const ItemsOwnersTableId = resourceToHex({
type: 'table',
namespace: ITEMS_NAMESPACE,
@@ -30,7 +37,7 @@ const ItemsOwnersTableId = resourceToHex({
const ItemsTokenURITableId = resourceToHex({
type: 'table',
namespace: ITEMS_NAMESPACE,
- name: 'MetadataURI',
+ name: 'URIStorage',
});
export const externalTables = {
@@ -67,6 +74,15 @@ export const externalTables = {
value: { type: 'uint256' },
},
},
+ ItemsBaseURI: {
+ namespace: ITEMS_NAMESPACE,
+ name: 'MetadataURI',
+ tableId: ItemsBaseURITableId,
+ keySchema: {},
+ valueSchema: {
+ uri: { type: 'string' },
+ },
+ },
ItemsOwners: {
namespace: ITEMS_NAMESPACE,
name: 'Owners',
@@ -81,9 +97,11 @@ export const externalTables = {
},
ItemsTokenURI: {
namespace: ITEMS_NAMESPACE,
- name: 'MetadataURI',
+ name: 'URIStorage',
tableId: ItemsTokenURITableId,
- keySchema: {},
+ keySchema: {
+ tokenId: { type: 'uint256' },
+ },
valueSchema: {
uri: { type: 'string' },
},
diff --git a/packages/client/src/pages/Character.tsx b/packages/client/src/pages/Character.tsx
index 68c0978a7..0469b9d5d 100644
--- a/packages/client/src/pages/Character.tsx
+++ b/packages/client/src/pages/Character.tsx
@@ -13,8 +13,15 @@ import {
Entity,
getComponentValue,
getComponentValueStrict,
+ Has,
+ NotValue,
+ runQuery,
} from '@latticexyz/recs';
-import { encodeEntity, singletonEntity } from '@latticexyz/store-sync/recs';
+import {
+ decodeEntity,
+ encodeEntity,
+ singletonEntity,
+} from '@latticexyz/store-sync/recs';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useParams } from 'react-router-dom';
import { formatEther, hexToString } from 'viem';
@@ -27,7 +34,7 @@ import { useCharacter } from '../contexts/CharacterContext';
import { useMUD } from '../contexts/MUDContext';
import { useToast } from '../hooks/useToast';
import { fetchMetadataFromUri, uriToHttp } from '../utils/helpers';
-import type { Character, CharacterStats } from '../utils/types';
+import type { Character, StatsClasses, Weapon } from '../utils/types';
export const CharacterPage = (): JSX.Element => {
const { characterId } = useParams();
@@ -37,6 +44,9 @@ export const CharacterPage = (): JSX.Element => {
Characters,
CharactersTokenURI,
GoldBalances,
+ ItemsBaseURI,
+ ItemsOwners,
+ ItemsTokenURI,
Stats,
UltimateDominionConfig,
},
@@ -50,12 +60,10 @@ export const CharacterPage = (): JSX.Element => {
singletonEntity,
);
- const [character, setCharacter] = useState<
- (Character & CharacterStats) | null
- >(null);
+ const [character, setCharacter] = useState(null);
const [isLoadingCharacter, setIsLoadingCharacter] = useState(true);
-
- const [, setIsLoadingItems] = useState(true);
+ const [items, setItems] = useState(null);
+ const [isLoadingItems, setIsLoadingItems] = useState(true);
const fetchCharacter = useCallback(async () => {
try {
@@ -67,7 +75,7 @@ export const CharacterPage = (): JSX.Element => {
worldContract
)
)
- return;
+ return null;
setIsLoadingCharacter(true);
const characterData = getComponentValue(
@@ -76,7 +84,7 @@ export const CharacterPage = (): JSX.Element => {
);
const characterStats = getComponentValue(Stats, characterId as Entity);
- if (!(characterData && characterStats)) return;
+ if (!(characterData && characterStats)) return null;
const ownerEntity = encodeEntity(
{ address: 'address' },
@@ -98,7 +106,7 @@ export const CharacterPage = (): JSX.Element => {
uriToHttp(`ipfs://${metadataURI}`)[0],
);
- setCharacter({
+ const _character = {
...fetachedMetadata,
agility: characterStats.agility.toString(),
baseHitPoints: characterStats.baseHitPoints.toString(),
@@ -115,9 +123,13 @@ export const CharacterPage = (): JSX.Element => {
owner: characterData.owner,
strength: characterStats.strength.toString(),
tokenId: characterData.tokenId.toString(),
- });
+ };
+
+ setCharacter(_character);
+ return _character;
} catch (error) {
renderError(error, 'Failed to fetch character data');
+ return null;
} finally {
setIsLoadingCharacter(false);
}
@@ -133,22 +145,97 @@ export const CharacterPage = (): JSX.Element => {
worldContract,
]);
- const fetchCharacterItems = useCallback(async () => {
- try {
- // eslint-disable-next-line no-console
- console.log('test');
- } catch (error) {
- renderError(error, 'Failed to fetch character data');
- } finally {
- setIsLoadingItems(false);
- }
- }, [renderError]);
+ const fetchCharacterItems = useCallback(
+ async (_character: Character) => {
+ try {
+ const _items = Array.from(
+ runQuery([
+ Has(ItemsOwners),
+ NotValue(ItemsOwners, { balance: BigInt(0) }),
+ ]),
+ )
+ .map(entity => {
+ const itemOwner = getComponentValueStrict(ItemsOwners, entity);
+ const { owner, tokenId } = decodeEntity(
+ { owner: 'address', tokenId: 'uint256' },
+ entity,
+ );
+
+ return {
+ balance: itemOwner.balance.toString(),
+ itemId: entity,
+ owner,
+ tokenId: tokenId.toString(),
+ };
+ })
+ .filter(item => item.owner === _character.owner)
+ .sort((a, b) => {
+ return Number(a.tokenId) - Number(b.tokenId);
+ });
+
+ const fullItems = await Promise.all(
+ _items.map(async item => {
+ const itemTemplateStats =
+ await worldContract.read.UD__getWeaponStats([
+ BigInt(item.tokenId),
+ ]);
+
+ const tokenIdEntity = encodeEntity(
+ { tokenId: 'uint256' },
+ { tokenId: BigInt(item.tokenId) },
+ );
+
+ const baseURI = getComponentValueStrict(
+ ItemsBaseURI,
+ singletonEntity,
+ ).uri;
+
+ const tokenURI = getComponentValueStrict(
+ ItemsTokenURI,
+ tokenIdEntity,
+ ).uri;
+
+ const metadata = await fetchMetadataFromUri(
+ uriToHttp(`${baseURI}${tokenURI}`)[0],
+ );
+
+ return {
+ ...metadata,
+ agiModifier: itemTemplateStats.agiModifier.toString(),
+ balance: item.balance,
+ classRestrictions: itemTemplateStats.classRestrictions.map(
+ (classRestriction: number) => classRestriction as StatsClasses,
+ ),
+ hitPointModifier: itemTemplateStats.hitPointModifier.toString(),
+ intModifier: itemTemplateStats.intModifier.toString(),
+ itemId: item.itemId,
+ maxDamage: itemTemplateStats.maxDamage.toString(),
+ minDamage: itemTemplateStats.minDamage.toString(),
+ minLevel: itemTemplateStats.minLevel.toString(),
+ owner: item.owner,
+ strModifier: itemTemplateStats.strModifier.toString(),
+ tokenId: item.tokenId,
+ } as Weapon;
+ }),
+ );
+
+ setItems(fullItems);
+ } catch (error) {
+ renderError(error, 'Failed to fetch character data');
+ } finally {
+ setIsLoadingItems(false);
+ }
+ },
+ [ItemsBaseURI, ItemsOwners, ItemsTokenURI, renderError, worldContract],
+ );
useEffect(() => {
if (!isSynced) return;
(async (): Promise => {
- await fetchCharacter();
- await fetchCharacterItems();
+ const _character = await fetchCharacter();
+
+ if (!_character) return;
+ await fetchCharacterItems(_character);
})();
}, [fetchCharacter, fetchCharacterItems, isSynced]);
@@ -244,36 +331,38 @@ export const CharacterPage = (): JSX.Element => {
rowSpan={{ base: 1, sm: 1, md: 1, lg: 1, xl: 1 }}
rowStart={{ base: 4, sm: 4, md: 4, lg: 2, xl: 2 }}
>
-
- Items 30 - 3/3 Equipped
-
-
- {DUMMY_ITEMS.map(function (item, i) {
- return (
-
- {/* TODO: we should only use one general modal, which gets passed the item data when clicked */}
-
-
- );
- })}
-
+ {items ? (
+ <>
+
+ Items {items.length} - 1/{items.length} equipped
+
+
+ {items.map(function (item, i) {
+ return (
+
+ {/* TODO: we should only use one general modal, which gets passed the item data when clicked */}
+
+
+ );
+ })}
+
+ >
+ ) : isLoadingItems ? (
+
+
+
+ ) : (
+ Error loading items
+ )}
) : (
@@ -303,87 +392,3 @@ export const CharacterPage = (): JSX.Element => {
);
};
-
-const DUMMY_ITEMS = [
- {
- agi: 3,
- disabled: false,
- icon: 'fire',
- image: 'door-closed',
- int: 4,
- name: 'Rusty Dagger',
- str: 1,
- },
- {
- agi: 3,
- disabled: false,
- icon: 'shield',
- image: 'scribd',
- int: 4,
- name: 'Copper Knife',
- str: 1,
- },
- {
- agi: 3,
- disabled: false,
- icon: 'road',
- image: 'database',
- int: 4,
- name: 'Iron Axe',
- str: 1,
- },
- {
- agi: 3,
- disabled: true,
- icon: 'fire',
- image: 'search',
- int: 4,
- name: 'Rusty Dagger',
- str: 1,
- },
- {
- agi: 3,
- disabled: true,
- icon: 'shield',
- image: 'book',
- int: 4,
- name: 'Rusty Dagger',
- str: 1,
- },
- {
- agi: 3,
- disabled: true,
- icon: 'road',
- image: 'pizza-slice',
- int: 4,
- name: 'Rusty Dagger',
- str: 1,
- },
- {
- agi: 3,
- disabled: true,
- icon: 'fire',
- image: 'star-crescent',
- int: 4,
- name: 'Rusty Dagger',
- str: 1,
- },
- {
- agi: 3,
- disabled: true,
- icon: 'shield',
- image: 'bug',
- int: 4,
- name: 'Rusty Dagger',
- str: 1,
- },
- {
- agi: 3,
- disabled: true,
- icon: 'road',
- image: 'socks',
- int: 4,
- name: 'Rusty Dagger',
- str: 1,
- },
-];
diff --git a/packages/client/src/pages/CharacterCreation.tsx b/packages/client/src/pages/CharacterCreation.tsx
index 679348905..50bb10d07 100644
--- a/packages/client/src/pages/CharacterCreation.tsx
+++ b/packages/client/src/pages/CharacterCreation.tsx
@@ -18,11 +18,11 @@ import {
VStack,
} from '@chakra-ui/react';
import { useComponentValue } from '@latticexyz/react';
-import { singletonEntity } from '@latticexyz/store-sync/recs';
+import { getComponentValueStrict } from '@latticexyz/recs';
+import { encodeEntity, 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';
-import { getContract } from 'viem';
import { useWalletClient } from 'wagmi';
import { useCharacter } from '../contexts/CharacterContext';
@@ -38,7 +38,7 @@ import {
} from '../utils/helpers';
import { StatsClasses, Weapon } from '../utils/types';
-const STARTER_WEAPON_IDS = [BigInt(1), BigInt(2), BigInt(3)];
+const STARTER_WEAPON_TOKEN_IDS = [BigInt(1), BigInt(2), BigInt(3)];
export const CharacterCreation = (): JSX.Element => {
const navigate = useNavigate();
@@ -47,10 +47,10 @@ export const CharacterCreation = (): JSX.Element => {
const { data: externalWalletClient } = useWalletClient();
const {
burnerBalance,
- components: { UltimateDominionConfig },
+ components: { ItemsBaseURI, ItemsTokenURI, UltimateDominionConfig },
delegatorAddress,
isSynced,
- network: { publicClient, worldContract },
+ network: { worldContract },
systemCalls: { enterGame, mintCharacter, rollStats },
} = useMUD();
const { character, isRefreshing, refreshCharacter } = useCharacter();
@@ -87,43 +87,28 @@ export const CharacterCreation = (): JSX.Element => {
const fetchStarterWeapons = useCallback(async () => {
try {
const _items: Weapon[] = await Promise.all(
- STARTER_WEAPON_IDS.map(async itemId => {
+ STARTER_WEAPON_TOKEN_IDS.map(async tokenId => {
const itemTemplateStats = await worldContract.read.UD__getWeaponStats(
- [itemId],
+ [tokenId],
);
- const itemsContractAddress =
- await worldContract.read.UD__getItemsContract();
-
- const itemsToken = getContract({
- address: itemsContractAddress,
- abi: [
- {
- constant: true,
- inputs: [
- {
- name: 'tokenId',
- type: 'uint256',
- },
- ],
- name: 'uri',
- outputs: [
- {
- name: '',
- type: 'string',
- },
- ],
- payable: false,
- stateMutability: 'view',
- type: 'function',
- },
- ],
- client: publicClient,
- });
-
- const metadataURI = await itemsToken.read.uri([itemId]);
+ const tokenIdEntity = encodeEntity(
+ { tokenId: 'uint256' },
+ { tokenId: tokenId },
+ );
+
+ const baseURI = getComponentValueStrict(
+ ItemsBaseURI,
+ singletonEntity,
+ ).uri;
+
+ const tokenURI = getComponentValueStrict(
+ ItemsTokenURI,
+ tokenIdEntity,
+ ).uri;
+
const fetachedMetadata = await fetchMetadataFromUri(
- uriToHttp(metadataURI)[0],
+ uriToHttp(`${baseURI}${tokenURI}`)[0],
);
return {
@@ -144,7 +129,7 @@ export const CharacterCreation = (): JSX.Element => {
} catch (error) {
renderError(error, 'Error fetching starter item.');
}
- }, [publicClient, renderError, worldContract]);
+ }, [ItemsBaseURI, ItemsTokenURI, renderError, worldContract]);
useEffect(() => {
fetchStarterWeapons();
@@ -225,7 +210,7 @@ export const CharacterCreation = (): JSX.Element => {
throw new Error('Contract call failed');
}
- refreshCharacter();
+ await refreshCharacter();
renderSuccess('Character created!');
} catch (e) {
renderError(e, 'Failed to create character.');
diff --git a/packages/client/src/utils/types.ts b/packages/client/src/utils/types.ts
index e612efc11..b0f8f8a54 100644
--- a/packages/client/src/utils/types.ts
+++ b/packages/client/src/utils/types.ts
@@ -39,7 +39,14 @@ export type Monster = Metadata & {
monsterId: Entity;
};
-export type Weapon = Metadata & {
+export type Weapon = WeaponStats &
+ Metadata & {
+ balance: string;
+ itemId: Entity;
+ tokenId: string;
+ };
+
+export type WeaponStats = {
agiModifier: string;
classRestrictions: StatsClasses[];
hitPointModifier: string;
From 5d547fbbe2e4e65c23c3839fa72a2129a82b1fca Mon Sep 17 00:00:00 2001
From: ECWireless
Date: Sun, 14 Jul 2024 11:16:47 -0600
Subject: [PATCH 2/7] Remove Card folder in components
---
.../{Character/Card => }/ItemCard.tsx | 2 +-
.../{Character/Card => }/ItemEquipModal.tsx | 49 +++++--------------
packages/client/src/pages/Character.tsx | 2 +-
packages/client/src/utils/types.ts | 1 +
4 files changed, 15 insertions(+), 39 deletions(-)
rename packages/client/src/components/{Character/Card => }/ItemCard.tsx (95%)
rename packages/client/src/components/{Character/Card => }/ItemEquipModal.tsx (55%)
diff --git a/packages/client/src/components/Character/Card/ItemCard.tsx b/packages/client/src/components/ItemCard.tsx
similarity index 95%
rename from packages/client/src/components/Character/Card/ItemCard.tsx
rename to packages/client/src/components/ItemCard.tsx
index 5589221be..35f097624 100644
--- a/packages/client/src/components/Character/Card/ItemCard.tsx
+++ b/packages/client/src/components/ItemCard.tsx
@@ -8,7 +8,7 @@ import {
} from '@chakra-ui/react';
import { GiRogue } from 'react-icons/gi';
-import type { Weapon } from '../../../utils/types';
+import type { Weapon } from '../utils/types';
export const ItemCard = (weapon: Weapon): JSX.Element => {
const { agiModifier, intModifier, strModifier, name } = weapon;
diff --git a/packages/client/src/components/Character/Card/ItemEquipModal.tsx b/packages/client/src/components/ItemEquipModal.tsx
similarity index 55%
rename from packages/client/src/components/Character/Card/ItemEquipModal.tsx
rename to packages/client/src/components/ItemEquipModal.tsx
index cc09ad94e..45a31bc5c 100644
--- a/packages/client/src/components/Character/Card/ItemEquipModal.tsx
+++ b/packages/client/src/components/ItemEquipModal.tsx
@@ -10,29 +10,20 @@ import {
ModalOverlay,
useDisclosure,
} from '@chakra-ui/react';
+import { useMemo } from 'react';
+import { useCharacter } from '../contexts/CharacterContext';
+import type { Weapon } from '../utils/types';
import { ItemCard } from './ItemCard';
-export const ItemEquipModal = ({
- agi,
- disabled,
- icon,
- image,
- int,
- isOwner,
- name,
- str,
-}: {
- agi: number;
- disabled: boolean;
- icon: string;
- image: string;
- int: number;
- isOwner: boolean;
- name: string;
- str: number;
-}): JSX.Element => {
+export const ItemEquipModal = (weapon: Weapon): JSX.Element => {
const { isOpen, onClose, onOpen } = useDisclosure();
+ const { character } = useCharacter();
+
+ const isOwner = useMemo(
+ () => character?.owner === weapon.owner,
+ [character, weapon.owner],
+ );
return (
@@ -42,15 +33,7 @@ export const ItemEquipModal = ({
{isOwner ? 'Equip' : 'Make an offer'}
-
+
);
diff --git a/packages/client/src/pages/Character.tsx b/packages/client/src/pages/Character.tsx
index 0469b9d5d..63faacc74 100644
--- a/packages/client/src/pages/Character.tsx
+++ b/packages/client/src/pages/Character.tsx
@@ -26,10 +26,10 @@ import { useCallback, useEffect, useMemo, useState } from 'react';
import { useParams } from 'react-router-dom';
import { formatEther, hexToString } from 'viem';
-import { ItemCard } from '../components/Character/Card/ItemCard';
import { Misc } from '../components/Character/Misc';
import { Profile } from '../components/Character/Profile';
import { Stats as StatsPanel } from '../components/Character/Stats';
+import { ItemCard } from '../components/ItemCard';
import { useCharacter } from '../contexts/CharacterContext';
import { useMUD } from '../contexts/MUDContext';
import { useToast } from '../hooks/useToast';
diff --git a/packages/client/src/utils/types.ts b/packages/client/src/utils/types.ts
index b0f8f8a54..f76d1cff5 100644
--- a/packages/client/src/utils/types.ts
+++ b/packages/client/src/utils/types.ts
@@ -43,6 +43,7 @@ export type Weapon = WeaponStats &
Metadata & {
balance: string;
itemId: Entity;
+ owner: string;
tokenId: string;
};
From aa76da3498029e442110d59a269f0476eddac540 Mon Sep 17 00:00:00 2001
From: ECWireless
Date: Sun, 14 Jul 2024 11:38:33 -0600
Subject: [PATCH 3/7] Bring back ItemEquipModal
---
packages/client/src/components/ItemCard.tsx | 25 ++++++---
.../client/src/components/ItemEquipModal.tsx | 55 ++++++++++---------
packages/client/src/pages/Character.tsx | 22 +++++++-
3 files changed, 66 insertions(+), 36 deletions(-)
diff --git a/packages/client/src/components/ItemCard.tsx b/packages/client/src/components/ItemCard.tsx
index 35f097624..c869249fe 100644
--- a/packages/client/src/components/ItemCard.tsx
+++ b/packages/client/src/components/ItemCard.tsx
@@ -10,7 +10,14 @@ import { GiRogue } from 'react-icons/gi';
import type { Weapon } from '../utils/types';
-export const ItemCard = (weapon: Weapon): JSX.Element => {
+type ItemCardProps = Weapon & {
+ onClick?: () => void;
+};
+
+export const ItemCard: React.FC = ({
+ onClick,
+ ...weapon
+}): JSX.Element => {
const { agiModifier, intModifier, strModifier, name } = weapon;
const disabled = false;
@@ -19,17 +26,17 @@ export const ItemCard = (weapon: Weapon): JSX.Element => {
diff --git a/packages/client/src/components/ItemEquipModal.tsx b/packages/client/src/components/ItemEquipModal.tsx
index 45a31bc5c..942a82efb 100644
--- a/packages/client/src/components/ItemEquipModal.tsx
+++ b/packages/client/src/components/ItemEquipModal.tsx
@@ -1,5 +1,4 @@
import {
- Box,
Button,
Modal,
ModalBody,
@@ -8,7 +7,7 @@ import {
ModalFooter,
ModalHeader,
ModalOverlay,
- useDisclosure,
+ Text,
} from '@chakra-ui/react';
import { useMemo } from 'react';
@@ -16,8 +15,16 @@ import { useCharacter } from '../contexts/CharacterContext';
import type { Weapon } from '../utils/types';
import { ItemCard } from './ItemCard';
-export const ItemEquipModal = (weapon: Weapon): JSX.Element => {
- const { isOpen, onClose, onOpen } = useDisclosure();
+type ItemEquipModalProps = Weapon & {
+ isOpen: boolean;
+ onClose: () => void;
+};
+
+export const ItemEquipModal: React.FC = ({
+ isOpen,
+ onClose,
+ ...weapon
+}): JSX.Element => {
const { character } = useCharacter();
const isOwner = useMemo(
@@ -26,26 +33,24 @@ export const ItemEquipModal = (weapon: Weapon): JSX.Element => {
);
return (
-
-
-
-
- {isOwner ? 'Equip' : 'Make an offer'}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+ {isOwner ? 'Equip Item' : 'Make an offer'}
+
+
+ Do you want to equip this item?
+
+
+
+
+
+
+
+
);
};
diff --git a/packages/client/src/pages/Character.tsx b/packages/client/src/pages/Character.tsx
index 63faacc74..2fc6759f1 100644
--- a/packages/client/src/pages/Character.tsx
+++ b/packages/client/src/pages/Character.tsx
@@ -7,6 +7,7 @@ import {
GridItem,
Spinner,
Text,
+ useDisclosure,
} from '@chakra-ui/react';
import { useComponentValue } from '@latticexyz/react';
import {
@@ -30,6 +31,7 @@ import { Misc } from '../components/Character/Misc';
import { Profile } from '../components/Character/Profile';
import { Stats as StatsPanel } from '../components/Character/Stats';
import { ItemCard } from '../components/ItemCard';
+import { ItemEquipModal } from '../components/ItemEquipModal';
import { useCharacter } from '../contexts/CharacterContext';
import { useMUD } from '../contexts/MUDContext';
import { useToast } from '../hooks/useToast';
@@ -55,6 +57,8 @@ export const CharacterPage = (): JSX.Element => {
} = useMUD();
const { character: userCharacter } = useCharacter();
+ const { isOpen, onClose, onOpen } = useDisclosure();
+
const ultimateDominionConfig = useComponentValue(
UltimateDominionConfig,
singletonEntity,
@@ -64,6 +68,7 @@ export const CharacterPage = (): JSX.Element => {
const [isLoadingCharacter, setIsLoadingCharacter] = useState(true);
const [items, setItems] = useState(null);
const [isLoadingItems, setIsLoadingItems] = useState(true);
+ const [selectedItem, setSelectedItem] = useState(null);
const fetchCharacter = useCallback(async () => {
try {
@@ -349,8 +354,13 @@ export const CharacterPage = (): JSX.Element => {
{items.map(function (item, i) {
return (
- {/* TODO: we should only use one general modal, which gets passed the item data when clicked */}
-
+ {
+ setSelectedItem(item);
+ onOpen();
+ }}
+ />
);
})}
@@ -389,6 +399,14 @@ export const CharacterPage = (): JSX.Element => {
)}
+ {
+ onClose();
+ setSelectedItem(null);
+ }}
+ {...(selectedItem as Weapon)}
+ />
);
};
From 64eed3150b51d0bd6eaac1b09dd405237f73c53f Mon Sep 17 00:00:00 2001
From: ECWireless
Date: Sun, 14 Jul 2024 16:03:36 -0600
Subject: [PATCH 4/7] Add equipping functionality
---
packages/client/src/components/ItemCard.tsx | 11 +--
.../client/src/components/ItemEquipModal.tsx | 74 ++++++++++++++++++-
.../client/src/lib/mud/createSystemCalls.ts | 17 +++++
packages/client/src/pages/Character.tsx | 47 ++++++------
4 files changed, 119 insertions(+), 30 deletions(-)
diff --git a/packages/client/src/components/ItemCard.tsx b/packages/client/src/components/ItemCard.tsx
index c869249fe..bc5fd7b78 100644
--- a/packages/client/src/components/ItemCard.tsx
+++ b/packages/client/src/components/ItemCard.tsx
@@ -11,30 +11,31 @@ import { GiRogue } from 'react-icons/gi';
import type { Weapon } from '../utils/types';
type ItemCardProps = Weapon & {
+ isEquipped?: boolean;
onClick?: () => void;
};
export const ItemCard: React.FC = ({
+ isEquipped = false,
onClick,
...weapon
}): JSX.Element => {
const { agiModifier, intModifier, strModifier, name } = weapon;
- const disabled = false;
-
return (
diff --git a/packages/client/src/components/ItemEquipModal.tsx b/packages/client/src/components/ItemEquipModal.tsx
index 942a82efb..60963e863 100644
--- a/packages/client/src/components/ItemEquipModal.tsx
+++ b/packages/client/src/components/ItemEquipModal.tsx
@@ -9,9 +9,11 @@ import {
ModalOverlay,
Text,
} from '@chakra-ui/react';
-import { useMemo } from 'react';
+import { useCallback, useMemo, useState } from 'react';
import { useCharacter } from '../contexts/CharacterContext';
+import { useMUD } from '../contexts/MUDContext';
+import { useToast } from '../hooks/useToast';
import type { Weapon } from '../utils/types';
import { ItemCard } from './ItemCard';
@@ -25,13 +27,72 @@ export const ItemEquipModal: React.FC = ({
onClose,
...weapon
}): JSX.Element => {
- const { character } = useCharacter();
+ const { renderError, renderSuccess } = useToast();
+ const {
+ burnerBalance,
+ delegatorAddress,
+ systemCalls: { equipItems },
+ } = useMUD();
+ const { character, refreshCharacter } = useCharacter();
+
+ const [isEquipping, setIsEquipping] = useState(false);
const isOwner = useMemo(
() => character?.owner === weapon.owner,
[character, weapon.owner],
);
+ const onEquipItem = useCallback(
+ async (e: React.FormEvent) => {
+ e.preventDefault();
+
+ try {
+ setIsEquipping(true);
+
+ if (!character) {
+ throw new Error('Character not found.');
+ }
+
+ if (burnerBalance === '0') {
+ throw new Error(
+ 'Insufficient funds. Please top off your session account.',
+ );
+ }
+
+ if (!delegatorAddress) {
+ throw new Error('Missing delegation.');
+ }
+
+ const success = await equipItems(character.characterId, [
+ weapon.tokenId,
+ ]);
+
+ if (!success) {
+ throw new Error('Contract call failed');
+ }
+
+ await refreshCharacter();
+ renderSuccess(`${weapon.name} equipped successfully!`);
+ onClose();
+ } catch (e) {
+ renderError(e, 'Failed to equip item.');
+ } finally {
+ setIsEquipping(false);
+ }
+ },
+ [
+ burnerBalance,
+ character,
+ delegatorAddress,
+ equipItems,
+ onClose,
+ refreshCharacter,
+ renderError,
+ renderSuccess,
+ weapon,
+ ],
+ );
+
return (
@@ -43,10 +104,15 @@ export const ItemEquipModal: React.FC = ({
-
diff --git a/packages/client/src/lib/mud/createSystemCalls.ts b/packages/client/src/lib/mud/createSystemCalls.ts
index eb550561a..fae389373 100644
--- a/packages/client/src/lib/mud/createSystemCalls.ts
+++ b/packages/client/src/lib/mud/createSystemCalls.ts
@@ -63,6 +63,22 @@ export function createSystemCalls(
}
};
+ const equipItems = async (characterEntity: Entity, itemIds: string[]) => {
+ try {
+ const tx = await worldContract.write.UD__equipItems([
+ characterEntity.toString() as `0x${string}`,
+ itemIds.map(itemId => BigInt(itemId)),
+ ]);
+
+ await waitForTransaction(tx);
+
+ const success = !!getComponentValue(Characters, characterEntity);
+ return success;
+ } catch (e) {
+ return false;
+ }
+ };
+
const mintCharacter = async (account: Address, name: string, uri: string) => {
try {
const nameHex = stringToHex(name, { size: 32 });
@@ -180,6 +196,7 @@ export function createSystemCalls(
return {
enterGame,
+ equipItems,
mintCharacter,
move,
rollStats,
diff --git a/packages/client/src/pages/Character.tsx b/packages/client/src/pages/Character.tsx
index 2fc6759f1..c1d72043a 100644
--- a/packages/client/src/pages/Character.tsx
+++ b/packages/client/src/pages/Character.tsx
@@ -38,11 +38,14 @@ import { useToast } from '../hooks/useToast';
import { fetchMetadataFromUri, uriToHttp } from '../utils/helpers';
import type { Character, StatsClasses, Weapon } from '../utils/types';
+const MAX_EQUIPPED_WEAPONS = 3;
+
export const CharacterPage = (): JSX.Element => {
const { characterId } = useParams();
const { renderError } = useToast();
const {
components: {
+ CharacterEquipment,
Characters,
CharactersTokenURI,
GoldBalances,
@@ -50,7 +53,6 @@ export const CharacterPage = (): JSX.Element => {
ItemsOwners,
ItemsTokenURI,
Stats,
- UltimateDominionConfig,
},
isSynced,
network: { publicClient, worldContract },
@@ -59,28 +61,19 @@ export const CharacterPage = (): JSX.Element => {
const { isOpen, onClose, onOpen } = useDisclosure();
- const ultimateDominionConfig = useComponentValue(
- UltimateDominionConfig,
- singletonEntity,
- );
-
const [character, setCharacter] = useState(null);
const [isLoadingCharacter, setIsLoadingCharacter] = useState(true);
const [items, setItems] = useState(null);
const [isLoadingItems, setIsLoadingItems] = useState(true);
const [selectedItem, setSelectedItem] = useState(null);
+ const equippedWeapons =
+ useComponentValue(CharacterEquipment, characterId as Entity | undefined)
+ ?.equippedWeapons ?? [];
+
const fetchCharacter = useCallback(async () => {
try {
- if (
- !(
- characterId &&
- publicClient &&
- ultimateDominionConfig &&
- worldContract
- )
- )
- return null;
+ if (!(characterId && publicClient && worldContract)) return null;
setIsLoadingCharacter(true);
const characterData = getComponentValue(
@@ -146,7 +139,6 @@ export const CharacterPage = (): JSX.Element => {
Stats,
publicClient,
renderError,
- ultimateDominionConfig,
worldContract,
]);
@@ -249,6 +241,8 @@ export const CharacterPage = (): JSX.Element => {
[character, userCharacter],
);
+ const maxItemsEquipped = equippedWeapons.length === MAX_EQUIPPED_WEAPONS;
+
if (isLoadingCharacter) {
return (
@@ -339,8 +333,12 @@ export const CharacterPage = (): JSX.Element => {
{items ? (
<>
- Items {items.length} - 1/{items.length} equipped
+ Items {items.length} - {equippedWeapons.length}/
+ {MAX_EQUIPPED_WEAPONS} equipped{' '}
+ {maxItemsEquipped && (
+ (Max items equipped)
+ )}
{
{
- setSelectedItem(item);
- onOpen();
- }}
+ isEquipped={equippedWeapons.includes(
+ BigInt(item.tokenId),
+ )}
+ onClick={
+ maxItemsEquipped
+ ? undefined
+ : () => {
+ setSelectedItem(item);
+ onOpen();
+ }
+ }
/>
);
From 1c6511f9a705c35a64617674d45d22f78c51969d Mon Sep 17 00:00:00 2001
From: ECWireless
Date: Sun, 14 Jul 2024 16:57:34 -0600
Subject: [PATCH 5/7] Add ability to unequip items
---
.../client/src/components/ItemEquipModal.tsx | 87 ++++++++++++++++++-
.../client/src/lib/mud/createSystemCalls.ts | 45 +++++++++-
packages/client/src/pages/Character.tsx | 12 ++-
3 files changed, 134 insertions(+), 10 deletions(-)
diff --git a/packages/client/src/components/ItemEquipModal.tsx b/packages/client/src/components/ItemEquipModal.tsx
index 60963e863..e90f48a7e 100644
--- a/packages/client/src/components/ItemEquipModal.tsx
+++ b/packages/client/src/components/ItemEquipModal.tsx
@@ -18,11 +18,13 @@ import type { Weapon } from '../utils/types';
import { ItemCard } from './ItemCard';
type ItemEquipModalProps = Weapon & {
+ isEquipped: boolean;
isOpen: boolean;
onClose: () => void;
};
export const ItemEquipModal: React.FC = ({
+ isEquipped,
isOpen,
onClose,
...weapon
@@ -31,9 +33,9 @@ export const ItemEquipModal: React.FC = ({
const {
burnerBalance,
delegatorAddress,
- systemCalls: { equipItems },
+ systemCalls: { equipItems, unequipItem },
} = useMUD();
- const { character, refreshCharacter } = useCharacter();
+ const { character } = useCharacter();
const [isEquipping, setIsEquipping] = useState(false);
@@ -71,7 +73,6 @@ export const ItemEquipModal: React.FC = ({
throw new Error('Contract call failed');
}
- await refreshCharacter();
renderSuccess(`${weapon.name} equipped successfully!`);
onClose();
} catch (e) {
@@ -86,13 +87,91 @@ export const ItemEquipModal: React.FC = ({
delegatorAddress,
equipItems,
onClose,
- refreshCharacter,
renderError,
renderSuccess,
weapon,
],
);
+ const onUnequipItem = useCallback(
+ async (e: React.FormEvent) => {
+ e.preventDefault();
+
+ try {
+ setIsEquipping(true);
+
+ if (!character) {
+ throw new Error('Character not found.');
+ }
+
+ if (burnerBalance === '0') {
+ throw new Error(
+ 'Insufficient funds. Please top off your session account.',
+ );
+ }
+
+ if (!delegatorAddress) {
+ throw new Error('Missing delegation.');
+ }
+
+ const success = await unequipItem(
+ character.characterId,
+ weapon.tokenId,
+ );
+
+ if (!success) {
+ throw new Error('Contract call failed');
+ }
+
+ renderSuccess(`${weapon.name} unequipped successfully!`);
+ onClose();
+ } catch (e) {
+ renderError(e, 'Failed to unequip item.');
+ } finally {
+ setIsEquipping(false);
+ }
+ },
+ [
+ burnerBalance,
+ character,
+ delegatorAddress,
+ onClose,
+ renderError,
+ renderSuccess,
+ unequipItem,
+ weapon,
+ ],
+ );
+
+ if (isEquipped) {
+ return (
+
+
+
+ Unequip Item
+
+
+ Do you want to unequip this item?
+
+
+
+
+ Yes
+
+
+ No
+
+
+
+
+ );
+ }
+
return (
diff --git a/packages/client/src/lib/mud/createSystemCalls.ts b/packages/client/src/lib/mud/createSystemCalls.ts
index fae389373..b872e3bf2 100644
--- a/packages/client/src/lib/mud/createSystemCalls.ts
+++ b/packages/client/src/lib/mud/createSystemCalls.ts
@@ -46,7 +46,7 @@ export function createSystemCalls(
* (https://github.com/latticexyz/mud/blob/main/templates/react/packages/client/src/mud/setupNetwork.ts#L77-L83).
*/
{ publicClient, waitForTransaction, worldContract }: SetupNetworkResult,
- { Characters, Position, Spawned }: ClientComponents,
+ { CharacterEquipment, Characters, Position, Spawned }: ClientComponents,
) {
const enterGame = async (characterEntity: Entity) => {
try {
@@ -72,7 +72,18 @@ export function createSystemCalls(
await waitForTransaction(tx);
- const success = !!getComponentValue(Characters, characterEntity);
+ const characterEquipment = getComponentValue(
+ CharacterEquipment,
+ characterEntity,
+ );
+
+ if (!characterEquipment) return false;
+ const { equippedArmor, equippedWeapons } = characterEquipment;
+
+ const success =
+ equippedArmor.some(id => itemIds.includes(id.toString())) ||
+ equippedWeapons.some(id => itemIds.includes(id.toString()));
+
return success;
} catch (e) {
return false;
@@ -194,6 +205,35 @@ export function createSystemCalls(
}
};
+ const unequipItem = async (characterEntity: Entity, itemId: string) => {
+ try {
+ const tx = await worldContract.write.UD__unequipItem([
+ characterEntity.toString() as `0x${string}`,
+ BigInt(itemId),
+ ]);
+
+ await waitForTransaction(tx);
+
+ const characterEquipment = getComponentValue(
+ CharacterEquipment,
+ characterEntity,
+ );
+
+ if (!characterEquipment) return false;
+
+ const { equippedArmor, equippedWeapons } = characterEquipment;
+
+ const success = !(
+ equippedArmor.includes(BigInt(itemId)) ||
+ equippedWeapons.includes(BigInt(itemId))
+ );
+
+ return success;
+ } catch (e) {
+ return false;
+ }
+ };
+
return {
enterGame,
equipItems,
@@ -201,5 +241,6 @@ export function createSystemCalls(
move,
rollStats,
spawn,
+ unequipItem,
};
}
diff --git a/packages/client/src/pages/Character.tsx b/packages/client/src/pages/Character.tsx
index c1d72043a..cd8e92201 100644
--- a/packages/client/src/pages/Character.tsx
+++ b/packages/client/src/pages/Character.tsx
@@ -350,15 +350,16 @@ export const CharacterPage = (): JSX.Element => {
mt={4}
>
{items.map(function (item, i) {
+ const isEquipped = equippedWeapons.includes(
+ BigInt(item.tokenId),
+ );
return (
{
setSelectedItem(item);
@@ -405,6 +406,9 @@ export const CharacterPage = (): JSX.Element => {
)}
{
onClose();
From 63da505717e9222e699128757b11e492f227e4de Mon Sep 17 00:00:00 2001
From: ECWireless
Date: Sun, 14 Jul 2024 18:44:51 -0600
Subject: [PATCH 6/7] Render items to StatsPanel
---
packages/client/src/components/StatsPanel.tsx | 192 +++++++++++++++---
packages/client/src/pages/Character.tsx | 3 +-
packages/client/src/utils/constants.ts | 2 +
3 files changed, 166 insertions(+), 31 deletions(-)
diff --git a/packages/client/src/components/StatsPanel.tsx b/packages/client/src/components/StatsPanel.tsx
index 6f66d90a8..f8145c06e 100644
--- a/packages/client/src/components/StatsPanel.tsx
+++ b/packages/client/src/components/StatsPanel.tsx
@@ -12,25 +12,42 @@ import {
VStack,
} from '@chakra-ui/react';
import { useComponentValue } from '@latticexyz/react';
-import { encodeEntity } from '@latticexyz/store-sync/recs';
-import { useMemo } from 'react';
+import { getComponentValue, getComponentValueStrict } from '@latticexyz/recs';
+import { encodeEntity, singletonEntity } from '@latticexyz/store-sync/recs';
+import { useCallback, useEffect, useMemo, useState } from 'react';
+import { GiRogue } from 'react-icons/gi';
import { IoIosArrowForward } from 'react-icons/io';
import { useNavigate } from 'react-router-dom';
import { useCharacter } from '../contexts/CharacterContext';
import { useMUD } from '../contexts/MUDContext';
+import { useToast } from '../hooks/useToast';
+import { MAX_EQUIPPED_WEAPONS } from '../utils/constants';
+import { fetchMetadataFromUri, uriToHttp } from '../utils/helpers';
+import type { Character, StatsClasses, Weapon } from '../utils/types';
import { Level } from './Level';
const CURRENT_LEVEL = 1;
export const StatsPanel = (): JSX.Element => {
const navigate = useNavigate();
+ const { renderError } = useToast();
const isDesktop = useBreakpointValue({ base: false, lg: true });
const {
- components: { Levels },
+ components: {
+ CharacterEquipment,
+ ItemsBaseURI,
+ ItemsOwners,
+ ItemsTokenURI,
+ Levels,
+ },
+ isSynced,
+ network: { worldContract },
} = useMUD();
const { character } = useCharacter();
+ const [items, setItems] = useState(null);
+
const nextLevelXpRequirement = useComponentValue(
Levels,
encodeEntity({ level: 'uint256' }, { level: BigInt(CURRENT_LEVEL + 1) }),
@@ -43,7 +60,106 @@ export const StatsPanel = (): JSX.Element => {
);
}, [character, nextLevelXpRequirement]);
- if (!character) {
+ const fetchCharacterItems = useCallback(
+ async (_character: Character, _equippedWeapons: bigint[]) => {
+ try {
+ if (_equippedWeapons.length === 0) {
+ setItems([]);
+ return;
+ }
+
+ const _items = _equippedWeapons
+ .map(tokenId => {
+ const tokenOwnersEntity = encodeEntity(
+ { owner: 'address', tokenId: 'uint256' },
+ {
+ owner: _character.owner as `0x${string}`,
+ tokenId: BigInt(tokenId),
+ },
+ );
+ const itemOwner = getComponentValueStrict(
+ ItemsOwners,
+ tokenOwnersEntity,
+ );
+
+ return {
+ balance: itemOwner.balance.toString(),
+ itemId: tokenOwnersEntity,
+ owner: _character.owner,
+ tokenId: tokenId.toString(),
+ };
+ })
+ .filter(item => item.owner === _character.owner)
+ .sort((a, b) => {
+ return Number(a.tokenId) - Number(b.tokenId);
+ });
+
+ const fullItems = await Promise.all(
+ _items.map(async item => {
+ const itemTemplateStats =
+ await worldContract.read.UD__getWeaponStats([
+ BigInt(item.tokenId),
+ ]);
+
+ const tokenIdEntity = encodeEntity(
+ { tokenId: 'uint256' },
+ { tokenId: BigInt(item.tokenId) },
+ );
+
+ const baseURI = getComponentValueStrict(
+ ItemsBaseURI,
+ singletonEntity,
+ ).uri;
+
+ const tokenURI = getComponentValueStrict(
+ ItemsTokenURI,
+ tokenIdEntity,
+ ).uri;
+
+ const metadata = await fetchMetadataFromUri(
+ uriToHttp(`${baseURI}${tokenURI}`)[0],
+ );
+
+ return {
+ ...metadata,
+ agiModifier: itemTemplateStats.agiModifier.toString(),
+ balance: item.balance,
+ classRestrictions: itemTemplateStats.classRestrictions.map(
+ (classRestriction: number) => classRestriction as StatsClasses,
+ ),
+ hitPointModifier: itemTemplateStats.hitPointModifier.toString(),
+ intModifier: itemTemplateStats.intModifier.toString(),
+ itemId: item.itemId,
+ maxDamage: itemTemplateStats.maxDamage.toString(),
+ minDamage: itemTemplateStats.minDamage.toString(),
+ minLevel: itemTemplateStats.minLevel.toString(),
+ owner: item.owner,
+ strModifier: itemTemplateStats.strModifier.toString(),
+ tokenId: item.tokenId,
+ } as Weapon;
+ }),
+ );
+
+ setItems(fullItems);
+ } catch (error) {
+ renderError(error, 'Failed to fetch character data');
+ }
+ },
+ [ItemsBaseURI, ItemsOwners, ItemsTokenURI, renderError, worldContract],
+ );
+
+ useEffect(() => {
+ if (!isSynced) return;
+ (async (): Promise => {
+ if (!character) return;
+ const equippedWeapons =
+ getComponentValue(CharacterEquipment, character.characterId)
+ ?.equippedWeapons ?? [];
+ await fetchCharacterItems(character, equippedWeapons);
+ })();
+ }, [character, CharacterEquipment, fetchCharacterItems, isSynced]);
+
+ if (!(character && items)) {
return (
@@ -129,32 +245,50 @@ export const StatsPanel = (): JSX.Element => {
Active Items
- 1/3
-
-
- Rusty Dagger
-
- ⁂
-
-
-
- Empty Slot
-
- +
-
-
-
- Empty Slot
-
- +
-
+
+ {items.length}/{MAX_EQUIPPED_WEAPONS}
+
+ {items.map((item, index) => (
+
+ {item.name}
+ navigate(`/characters/${character.characterId}`)}
+ p="0 2px"
+ size="sm"
+ variant="ghost"
+ >
+
+
+
+ ))}
+ {Array.from({
+ length: MAX_EQUIPPED_WEAPONS - items.length,
+ }).map((_, index) => (
+
+ Empty Slot
+ navigate(`/characters/${character.characterId}`)}
+ p="0 2px"
+ size="sm"
+ variant="ghost"
+ >
+ +
+
+
+ ))}
diff --git a/packages/client/src/pages/Character.tsx b/packages/client/src/pages/Character.tsx
index cd8e92201..42079d5fa 100644
--- a/packages/client/src/pages/Character.tsx
+++ b/packages/client/src/pages/Character.tsx
@@ -35,11 +35,10 @@ import { ItemEquipModal } from '../components/ItemEquipModal';
import { useCharacter } from '../contexts/CharacterContext';
import { useMUD } from '../contexts/MUDContext';
import { useToast } from '../hooks/useToast';
+import { MAX_EQUIPPED_WEAPONS } from '../utils/constants';
import { fetchMetadataFromUri, uriToHttp } from '../utils/helpers';
import type { Character, StatsClasses, Weapon } from '../utils/types';
-const MAX_EQUIPPED_WEAPONS = 3;
-
export const CharacterPage = (): JSX.Element => {
const { characterId } = useParams();
const { renderError } = useToast();
diff --git a/packages/client/src/utils/constants.ts b/packages/client/src/utils/constants.ts
index e6002d9d0..5a0dedc7b 100644
--- a/packages/client/src/utils/constants.ts
+++ b/packages/client/src/utils/constants.ts
@@ -1 +1,3 @@
export const API_URL = import.meta.env.VITE_API_URL ?? 'http://localhost:8080';
+
+export const MAX_EQUIPPED_WEAPONS = 3;
From edffe2d4b9d83582df7b7484c80bf054c78cefa6 Mon Sep 17 00:00:00 2001
From: ECWireless
Date: Sun, 14 Jul 2024 18:53:31 -0600
Subject: [PATCH 7/7] Link to other players from TileDetailsPanel
---
.../client/src/components/ItemEquipModal.tsx | 20 ++++++++++++++-----
packages/client/src/components/MapPanel.tsx | 16 ++++++++++++---
.../src/components/TileDetailsPanel.tsx | 7 ++++++-
3 files changed, 34 insertions(+), 9 deletions(-)
diff --git a/packages/client/src/components/ItemEquipModal.tsx b/packages/client/src/components/ItemEquipModal.tsx
index e90f48a7e..9fe5687b3 100644
--- a/packages/client/src/components/ItemEquipModal.tsx
+++ b/packages/client/src/components/ItemEquipModal.tsx
@@ -148,10 +148,16 @@ export const ItemEquipModal: React.FC = ({
- Unequip Item
+
+ {isOwner ? 'Unequip Item' : 'Make an offer'}
+
- Do you want to unequip this item?
+ {isOwner ? (
+ Do you want to unequip this item?
+ ) : (
+ Do you want to make an offer for this item?
+ )}
@@ -159,7 +165,7 @@ export const ItemEquipModal: React.FC = ({
isLoading={isEquipping}
loadingText="Unequipping..."
mr={3}
- onClick={onUnequipItem}
+ onClick={isOwner ? onUnequipItem : onClose}
>
Yes
@@ -179,7 +185,11 @@ export const ItemEquipModal: React.FC = ({
{isOwner ? 'Equip Item' : 'Make an offer'}
- Do you want to equip this item?
+ {isOwner ? (
+ Do you want to equip this item?
+ ) : (
+ Do you want to make an offer for this item?
+ )}
@@ -187,7 +197,7 @@ export const ItemEquipModal: React.FC = ({
isLoading={isEquipping}
loadingText="Equipping..."
mr={3}
- onClick={onEquipItem}
+ onClick={isOwner ? onEquipItem : onClose}
>
Yes
diff --git a/packages/client/src/components/MapPanel.tsx b/packages/client/src/components/MapPanel.tsx
index 8a60af12e..53276a7bd 100644
--- a/packages/client/src/components/MapPanel.tsx
+++ b/packages/client/src/components/MapPanel.tsx
@@ -16,8 +16,15 @@ const SAFE_ZONE_AREA = {
};
export const MapPanel = (): JSX.Element => {
- const { isRefreshing, isSpawned, isSpawning, onMove, onSpawn, position } =
- useMapNavigation();
+ const {
+ isRefreshing,
+ isSpawned,
+ isSpawning,
+ onMove,
+ onSpawn,
+ otherPlayers,
+ position,
+ } = useMapNavigation();
return (
{
)}
- Dark Cave - 2,344 Players
+
+ Dark Cave - {otherPlayers.length + 1} Player
+ {otherPlayers.length + 1 > 1 ? 's' : ''}
+
{isSpawned ? (
diff --git a/packages/client/src/components/TileDetailsPanel.tsx b/packages/client/src/components/TileDetailsPanel.tsx
index 4f30eb00b..6819916a2 100644
--- a/packages/client/src/components/TileDetailsPanel.tsx
+++ b/packages/client/src/components/TileDetailsPanel.tsx
@@ -10,6 +10,7 @@ import {
useBreakpointValue,
} from '@chakra-ui/react';
import { IoIosArrowForward } from 'react-icons/io';
+import { useNavigate } from 'react-router-dom';
import { useMapNavigation } from '../contexts/MapNavigationContext';
import { type Character, type Monster } from '../utils/types';
@@ -156,10 +157,14 @@ const PlayerRow = ({ player }: { player: Character }) => {
};
const PlayerLevelRow = ({ player }: { player: Character }) => {
+ const navigate = useNavigate();
const isMobile = useBreakpointValue({ base: true, md: false });
return (
-
+ navigate(`/characters/${player.characterId}`)}
+ >