From bfa70136b7fafbd3bff57059785db91356cbce1d Mon Sep 17 00:00:00 2001
From: ECWireless
Date: Sun, 28 Jul 2024 15:07:58 -0600
Subject: [PATCH 1/5] Improve how errors are rendered to the frontend
---
packages/client/src/components/ActionsPanel.tsx | 2 +-
packages/client/src/components/DelegationButton.tsx | 2 +-
packages/client/src/components/EditCharacterModal.tsx | 2 +-
packages/client/src/components/ItemEquipModal.tsx | 4 ++--
packages/client/src/components/TileDetailsPanel.tsx | 2 +-
packages/client/src/components/WalletDetailsModal.tsx | 4 ++--
packages/client/src/contexts/CharacterContext.tsx | 7 +++++--
packages/client/src/contexts/MapNavigationContext.tsx | 11 +++++++----
packages/client/src/pages/Character.tsx | 10 ++++++++--
packages/client/src/pages/CharacterCreation.tsx | 10 +++++-----
packages/client/src/pages/Leaderboard.tsx | 5 ++++-
11 files changed, 37 insertions(+), 22 deletions(-)
diff --git a/packages/client/src/components/ActionsPanel.tsx b/packages/client/src/components/ActionsPanel.tsx
index 9198500c2..6ddd91c44 100644
--- a/packages/client/src/components/ActionsPanel.tsx
+++ b/packages/client/src/components/ActionsPanel.tsx
@@ -250,7 +250,7 @@ export const ActionsPanel = (): JSX.Element => {
await refreshCharacter();
} catch (e) {
- renderError('Failed to attack.', e);
+ renderError((e as Error)?.message ?? 'Failed to attack.', e);
} finally {
setIsAttacking(false);
}
diff --git a/packages/client/src/components/DelegationButton.tsx b/packages/client/src/components/DelegationButton.tsx
index c26b45fe7..ba273bcb0 100644
--- a/packages/client/src/components/DelegationButton.tsx
+++ b/packages/client/src/components/DelegationButton.tsx
@@ -47,7 +47,7 @@ export const DelegationButton = ({
getBurner();
navigate(CHARACTER_CREATION_PATH);
} catch (e) {
- renderError('Failed to delegate.', e);
+ renderError((e as Error)?.message ?? 'Failed to delegate.', e);
} finally {
setIsDelegating(false);
}
diff --git a/packages/client/src/components/EditCharacterModal.tsx b/packages/client/src/components/EditCharacterModal.tsx
index 861cdbba4..73d54ccd1 100644
--- a/packages/client/src/components/EditCharacterModal.tsx
+++ b/packages/client/src/components/EditCharacterModal.tsx
@@ -233,7 +233,7 @@ export const EditCharacterModal: React.FC = ({
size="sm"
type="button"
>
- Upload Avatar
+ Upload Avatar Image
{showError && !(avatar || image) && (
diff --git a/packages/client/src/components/ItemEquipModal.tsx b/packages/client/src/components/ItemEquipModal.tsx
index 075a889de..602c36397 100644
--- a/packages/client/src/components/ItemEquipModal.tsx
+++ b/packages/client/src/components/ItemEquipModal.tsx
@@ -67,7 +67,7 @@ export const ItemEquipModal: React.FC = ({
renderSuccess(`${weapon.name} equipped successfully!`);
onClose();
} catch (e) {
- renderError('Failed to equip item.', e);
+ renderError((e as Error)?.message ?? 'Failed to equip item.', e);
} finally {
setIsEquipping(false);
}
@@ -107,7 +107,7 @@ export const ItemEquipModal: React.FC = ({
renderSuccess(`${weapon.name} unequipped successfully!`);
onClose();
} catch (e) {
- renderError('Failed to unequip item.', e);
+ renderError((e as Error)?.message ?? 'Failed to unequip item.', e);
} finally {
setIsEquipping(false);
}
diff --git a/packages/client/src/components/TileDetailsPanel.tsx b/packages/client/src/components/TileDetailsPanel.tsx
index 48349625b..f61915e75 100644
--- a/packages/client/src/components/TileDetailsPanel.tsx
+++ b/packages/client/src/components/TileDetailsPanel.tsx
@@ -76,7 +76,7 @@ export const TileDetailsPanel = (): JSX.Element => {
renderSuccess('Battle has begun!');
} catch (e) {
- renderError('Failed to initiate battle.', e);
+ renderError((e as Error)?.message ?? 'Failed to initiate battle.', e);
} finally {
setIsInitiating(false);
}
diff --git a/packages/client/src/components/WalletDetailsModal.tsx b/packages/client/src/components/WalletDetailsModal.tsx
index e5e972c03..4648d5090 100644
--- a/packages/client/src/components/WalletDetailsModal.tsx
+++ b/packages/client/src/components/WalletDetailsModal.tsx
@@ -96,7 +96,7 @@ export const WalletDetailsModal = ({
setDepositAmount('0');
renderSuccess('Funds deposited successfully!');
} catch (e) {
- renderError('Error depositing funds.', e);
+ renderError((e as Error)?.message ?? 'Error depositing funds.', e);
} finally {
setIsDepositing(false);
}
@@ -131,7 +131,7 @@ export const WalletDetailsModal = ({
setWithdrawAmount('0');
renderSuccess('Funds withdrawn successfully!');
} catch (e) {
- renderError('Error withdrawing funds.', e);
+ renderError((e as Error)?.message ?? 'Error withdrawing funds.', e);
} finally {
setIsWithdrawing(false);
}
diff --git a/packages/client/src/contexts/CharacterContext.tsx b/packages/client/src/contexts/CharacterContext.tsx
index 093d2b545..e3f4ebdfb 100644
--- a/packages/client/src/contexts/CharacterContext.tsx
+++ b/packages/client/src/contexts/CharacterContext.tsx
@@ -148,7 +148,7 @@ export const CharacterProvider = ({
try {
await fetchCharacterData();
} catch (e) {
- renderError('Error refreshing character.', e);
+ renderError((e as Error)?.message ?? 'Error refreshing character.', e);
} finally {
setIsRefreshing(false);
}
@@ -248,7 +248,10 @@ export const CharacterProvider = ({
setEquippedItems(fullItems);
} catch (e) {
- renderError('Failed to fetch character data.', e);
+ renderError(
+ (e as Error)?.message ?? 'Failed to fetch character data.',
+ e,
+ );
}
},
[ItemsBaseURI, ItemsOwners, ItemsTokenURI, renderError, worldContract],
diff --git a/packages/client/src/contexts/MapNavigationContext.tsx b/packages/client/src/contexts/MapNavigationContext.tsx
index ed5bd3175..4e97c7f2f 100644
--- a/packages/client/src/contexts/MapNavigationContext.tsx
+++ b/packages/client/src/contexts/MapNavigationContext.tsx
@@ -185,7 +185,10 @@ export const MapNavigationProvider = ({
setOtherPlayers(characters.filter(c => c.owner !== delegatorAddress));
} catch (e) {
- renderError('Failed to fetch other players.', e);
+ renderError(
+ (e as Error)?.message ?? 'Failed to fetch other players.',
+ e,
+ );
}
},
[
@@ -252,7 +255,7 @@ export const MapNavigationProvider = ({
setMonsters(_monsters.filter(m => Number(m.currentHp) > 0));
} catch (e) {
- renderError('Failed to fetch monsters.', e);
+ renderError((e as Error)?.message ?? 'Failed to fetch monsters.', e);
}
},
[MatchEntity, Mobs, renderError, Stats],
@@ -295,7 +298,7 @@ export const MapNavigationProvider = ({
renderSuccess('Spawned!');
} catch (e) {
- renderError('Failed to spawn.', e);
+ renderError((e as Error)?.message ?? 'Failed to spawn.', e);
} finally {
setIsSpawning(false);
}
@@ -359,7 +362,7 @@ export const MapNavigationProvider = ({
throw new Error(error);
}
} catch (e) {
- renderError('Failed to move.', e);
+ renderError((e as Error)?.message ?? 'Failed to move.', e);
} finally {
setIsMoving(false);
}
diff --git a/packages/client/src/pages/Character.tsx b/packages/client/src/pages/Character.tsx
index f1a552ec2..9a2f5ed91 100644
--- a/packages/client/src/pages/Character.tsx
+++ b/packages/client/src/pages/Character.tsx
@@ -163,7 +163,10 @@ export const CharacterPage = (): JSX.Element => {
setCharacter(_character);
return _character;
} catch (e) {
- renderError('Failed to fetch character data.', e);
+ renderError(
+ (e as Error)?.message ?? 'Failed to fetch character data.',
+ e,
+ );
return null;
} finally {
setIsLoadingCharacter(false);
@@ -263,7 +266,10 @@ export const CharacterPage = (): JSX.Element => {
setItems(fullItems);
} catch (e) {
- renderError('Failed to fetch character data.', e);
+ renderError(
+ (e as Error)?.message ?? 'Failed to fetch character items.',
+ e,
+ );
} finally {
setIsLoadingItems(false);
}
diff --git a/packages/client/src/pages/CharacterCreation.tsx b/packages/client/src/pages/CharacterCreation.tsx
index a18bee5e8..49a36895b 100644
--- a/packages/client/src/pages/CharacterCreation.tsx
+++ b/packages/client/src/pages/CharacterCreation.tsx
@@ -125,7 +125,7 @@ export const CharacterCreation = (): JSX.Element => {
setStarterWeapons(_items);
} catch (e) {
- renderError('Error fetching starter item.', e);
+ renderError((e as Error)?.message ?? 'Error fetching starter item.', e);
}
}, [ItemsBaseURI, ItemsTokenURI, renderError, worldContract]);
@@ -205,7 +205,7 @@ export const CharacterCreation = (): JSX.Element => {
await refreshCharacter();
renderSuccess('Character created!');
} catch (e) {
- renderError('Failed to create character.', e);
+ renderError((e as Error)?.message ?? 'Failed to create character.', e);
} finally {
setIsCreating(false);
}
@@ -247,7 +247,7 @@ export const CharacterCreation = (): JSX.Element => {
await refreshCharacter();
renderSuccess('Stats rolled!');
} catch (e) {
- renderError('Failed to roll stats.', e);
+ renderError((e as Error)?.message ?? 'Failed to roll stats.', e);
} finally {
setIsRollingStats(false);
}
@@ -290,7 +290,7 @@ export const CharacterCreation = (): JSX.Element => {
renderSuccess('Your character has awakend!');
navigate(GAME_BOARD_PATH);
} catch (e) {
- renderError('Failed to enter game.', e);
+ renderError((e as Error)?.message ?? 'Failed to enter game.', e);
} finally {
setIsEnteringGame(false);
}
@@ -437,7 +437,7 @@ export const CharacterCreation = (): JSX.Element => {
size="sm"
type="button"
>
- Upload Avatar
+ Upload Avatar Image
{showError && !avatar && (
diff --git a/packages/client/src/pages/Leaderboard.tsx b/packages/client/src/pages/Leaderboard.tsx
index 418b11cd5..a8a47d2b6 100644
--- a/packages/client/src/pages/Leaderboard.tsx
+++ b/packages/client/src/pages/Leaderboard.tsx
@@ -119,7 +119,10 @@ export const Leaderboard = (): JSX.Element => {
setCharacters(_characters);
} catch (e) {
- renderError('Failed to fetch other players.', e);
+ renderError(
+ (e as Error)?.message ?? 'Failed to fetch other players.',
+ e,
+ );
} finally {
setIsFetchingCharacters(false);
}
From b7260b9640bf16090333f17ee38033c2c9e84e87 Mon Sep 17 00:00:00 2001
From: ECWireless
Date: Sun, 28 Jul 2024 15:11:17 -0600
Subject: [PATCH 2/5] Make sure system call returns error if check fails
---
packages/client/src/lib/mud/createSystemCalls.ts | 10 ++++++++++
1 file changed, 10 insertions(+)
diff --git a/packages/client/src/lib/mud/createSystemCalls.ts b/packages/client/src/lib/mud/createSystemCalls.ts
index 3bb7e4ea3..b2e94eee4 100644
--- a/packages/client/src/lib/mud/createSystemCalls.ts
+++ b/packages/client/src/lib/mud/createSystemCalls.ts
@@ -135,6 +135,7 @@ export function createSystemCalls(
})[0];
return {
+ error: success ? undefined : 'Failed to create match.',
success,
};
} catch (e) {
@@ -199,6 +200,7 @@ export function createSystemCalls(
const success = currentTurn === BigInt(previousTurn) + BigInt(1);
return {
+ error: success ? undefined : 'Failed to end turn.',
success,
};
} catch (e) {
@@ -227,6 +229,7 @@ export function createSystemCalls(
const success = !!getComponentValue(Characters, characterEntity)?.locked;
return {
+ error: success ? undefined : 'Failed to enter game.',
success,
};
} catch (e) {
@@ -272,6 +275,7 @@ export function createSystemCalls(
equippedWeapons.some(id => itemIds.includes(id.toString()));
return {
+ error: success ? undefined : 'Failed to equip items.',
success,
};
} catch (e) {
@@ -322,6 +326,7 @@ export function createSystemCalls(
);
return {
+ error: success ? undefined : 'Failed to mint character.',
success,
};
} catch (e) {
@@ -367,6 +372,7 @@ export function createSystemCalls(
const success = x === newX && y === newY;
return {
+ error: success ? undefined : 'Failed to move.',
success,
};
} catch (e) {
@@ -425,6 +431,7 @@ export function createSystemCalls(
const success = !!getComponentValue(Stats, characterEntity);
return {
+ error: success ? undefined : 'Failed to roll stats.',
success,
};
} catch (e) {
@@ -454,6 +461,7 @@ export function createSystemCalls(
const success = !!getComponentValue(Spawned, characterEntity)?.spawned;
return {
+ error: success ? undefined : 'Failed to spawn.',
success,
};
} catch (e) {
@@ -497,6 +505,7 @@ export function createSystemCalls(
);
return {
+ error: success ? undefined : 'Failed to unequip item.',
success,
};
} catch (e) {
@@ -541,6 +550,7 @@ export function createSystemCalls(
const success = newMetadataURI === `ipfs://${characterMetadataCid}`;
return {
+ error: success ? undefined : 'Failed to update token URI.',
success,
};
} catch (e) {
From 072d677cc1efa99b6f1182917f7d16f7f27d0007 Mon Sep 17 00:00:00 2001
From: ECWireless
Date: Sun, 28 Jul 2024 15:26:16 -0600
Subject: [PATCH 3/5] Prevent keyboard movement if in battle
---
packages/client/src/App.tsx | 5 +-
.../client/src/components/ActionsPanel.tsx | 4 +-
packages/client/src/components/MapPanel.tsx | 3 +-
.../src/components/TileDetailsPanel.tsx | 10 ++-
.../client/src/contexts/CombatContext.tsx | 84 -------------------
.../src/contexts/MapNavigationContext.tsx | 52 +++++++++++-
.../client/src/lib/mud/createSystemCalls.ts | 10 ++-
7 files changed, 68 insertions(+), 100 deletions(-)
delete mode 100644 packages/client/src/contexts/CombatContext.tsx
diff --git a/packages/client/src/App.tsx b/packages/client/src/App.tsx
index 35dbfd7f0..9fd730be5 100644
--- a/packages/client/src/App.tsx
+++ b/packages/client/src/App.tsx
@@ -5,7 +5,6 @@ import { BrowserRouter as Router, useLocation } from 'react-router-dom';
import { Footer } from './components/Footer';
import { Header } from './components/Header';
import { WalletDetailsModal } from './components/WalletDetailsModal';
-import { CombatProvider } from './contexts/CombatContext';
import { MapNavigationProvider } from './contexts/MapNavigationContext';
import { useMUD } from './contexts/MUDContext';
import AppRoutes, { HOME_PATH } from './Routes';
@@ -14,9 +13,7 @@ export const App = (): JSX.Element => {
return (
-
-
-
+
);
diff --git a/packages/client/src/components/ActionsPanel.tsx b/packages/client/src/components/ActionsPanel.tsx
index 6ddd91c44..453be0fde 100644
--- a/packages/client/src/components/ActionsPanel.tsx
+++ b/packages/client/src/components/ActionsPanel.tsx
@@ -14,7 +14,6 @@ import Typist from 'react-typist';
import { formatUnits } from 'viem';
import { useCharacter } from '../contexts/CharacterContext';
-import { useCombat } from '../contexts/CombatContext';
import { useMapNavigation } from '../contexts/MapNavigationContext';
import { useMUD } from '../contexts/MUDContext';
import { useToast } from '../hooks/useToast';
@@ -76,12 +75,13 @@ export const ActionsPanel = (): JSX.Element => {
refreshCharacter,
} = useCharacter();
const {
+ currentBattle,
isRefreshing: isRefreshingMap,
isSpawned,
+ monsterOponent,
monsters,
position,
} = useMapNavigation();
- const { currentBattle, monsterOponent } = useCombat();
const [isAttacking, setIsAttacking] = useState(false);
diff --git a/packages/client/src/components/MapPanel.tsx b/packages/client/src/components/MapPanel.tsx
index 11792083d..081032b3c 100644
--- a/packages/client/src/components/MapPanel.tsx
+++ b/packages/client/src/components/MapPanel.tsx
@@ -8,7 +8,6 @@ import {
} from 'react-icons/io';
import { TbDirectionArrows } from 'react-icons/tb';
-import { useCombat } from '../contexts/CombatContext';
import { useMapNavigation } from '../contexts/MapNavigationContext';
const SAFE_ZONE_AREA = {
@@ -18,6 +17,7 @@ const SAFE_ZONE_AREA = {
export const MapPanel = (): JSX.Element => {
const {
+ currentBattle,
isRefreshing,
isSpawned,
isSpawning,
@@ -26,7 +26,6 @@ export const MapPanel = (): JSX.Element => {
otherPlayers,
position,
} = useMapNavigation();
- const { currentBattle } = useCombat();
return (
{
systemCalls: { createMatch },
} = useMUD();
const { character } = useCharacter();
- const { isRefreshing, monsters, otherPlayers } = useMapNavigation();
- const { currentBattle, monsterOponent } = useCombat();
+ const {
+ currentBattle,
+ isRefreshing,
+ monsterOponent,
+ monsters,
+ otherPlayers,
+ } = useMapNavigation();
const [isInitiating, setIsInitiating] = useState(false);
const [isMonsterHit, setIsMonsterHit] = useState(false);
diff --git a/packages/client/src/contexts/CombatContext.tsx b/packages/client/src/contexts/CombatContext.tsx
deleted file mode 100644
index 76853cca1..000000000
--- a/packages/client/src/contexts/CombatContext.tsx
+++ /dev/null
@@ -1,84 +0,0 @@
-import { useEntityQuery } from '@latticexyz/react';
-import { Entity, getComponentValue, Has, HasValue } from '@latticexyz/recs';
-import { createContext, ReactNode, useContext, useMemo } from 'react';
-
-import type { CombatDetails, Monster } from '../utils/types';
-import { useCharacter } from './CharacterContext';
-import { useMapNavigation } from './MapNavigationContext';
-import { useMUD } from './MUDContext';
-
-type CombatContextType = {
- currentBattle: CombatDetails | null;
- monsterOponent: Monster | null;
-};
-
-const CombatContext = createContext({
- currentBattle: null,
- monsterOponent: null,
-});
-
-export type NavigationProviderProps = {
- children: ReactNode;
-};
-
-export const CombatProvider = ({
- children,
-}: NavigationProviderProps): JSX.Element => {
- const {
- components: { CombatEncounter },
- } = useMUD();
- const { character } = useCharacter();
- const { monsters } = useMapNavigation();
-
- const currentBattle =
- Array.from(
- useEntityQuery([
- Has(CombatEncounter),
- HasValue(CombatEncounter, { end: BigInt(0) }),
- ]),
- )
- .map(entity => {
- const encounter = getComponentValue(CombatEncounter, entity);
- if (!encounter) return null;
-
- return {
- attackers: encounter.attackers as Entity[],
- currentTurn: encounter.currentTurn.toString(),
- defenders: encounter.defenders as Entity[],
- encounterId: entity,
- encounterType: encounter.encounterType,
- end: encounter.end.toString(),
- maxTurns: encounter.maxTurns.toString(),
- start: encounter.start.toString(),
- };
- })
- .filter(
- encounter =>
- character &&
- (encounter?.attackers.includes(character.characterId) ||
- encounter?.defenders.includes(character.characterId)),
- )[0] ?? null;
-
- const monsterOponent = useMemo(() => {
- if (!currentBattle) return null;
-
- return (
- monsters.find(monster =>
- currentBattle.defenders.includes(monster.monsterId),
- ) ?? null
- );
- }, [currentBattle, monsters]);
-
- return (
-
- {children}
-
- );
-};
-
-export const useCombat = (): CombatContextType => useContext(CombatContext);
diff --git a/packages/client/src/contexts/MapNavigationContext.tsx b/packages/client/src/contexts/MapNavigationContext.tsx
index 4e97c7f2f..bbaa8d2ec 100644
--- a/packages/client/src/contexts/MapNavigationContext.tsx
+++ b/packages/client/src/contexts/MapNavigationContext.tsx
@@ -14,6 +14,7 @@ import {
useCallback,
useContext,
useEffect,
+ useMemo,
useState,
} from 'react';
import { useLocation } from 'react-router-dom';
@@ -28,14 +29,16 @@ import {
import { useToast } from '../hooks/useToast';
import { GAME_BOARD_PATH } from '../Routes';
import { fetchMetadataFromUri, uriToHttp } from '../utils/helpers';
-import type { Character, Monster } from '../utils/types';
+import type { Character, CombatDetails, Monster } from '../utils/types';
import { useCharacter } from './CharacterContext';
import { useMUD } from './MUDContext';
type MapNavigationContextType = {
+ currentBattle: CombatDetails | null;
isRefreshing: boolean;
isSpawned: boolean;
isSpawning: boolean;
+ monsterOponent: Monster | null;
monsters: Monster[];
onMove: (direction: 'up' | 'down' | 'left' | 'right') => void;
onSpawn: () => void;
@@ -44,9 +47,11 @@ type MapNavigationContextType = {
};
const MapNavigationContext = createContext({
+ currentBattle: null,
isRefreshing: false,
isSpawned: false,
isSpawning: false,
+ monsterOponent: null,
monsters: [],
onMove: () => {},
onSpawn: () => {},
@@ -67,6 +72,7 @@ export const MapNavigationProvider = ({
components: {
Characters,
CharactersTokenURI,
+ CombatEncounter,
GoldBalances,
MatchEntity,
Mobs,
@@ -304,9 +310,39 @@ export const MapNavigationProvider = ({
}
}, [character, delegatorAddress, renderError, renderSuccess, spawn]);
+ const currentBattle =
+ Array.from(
+ useEntityQuery([
+ Has(CombatEncounter),
+ HasValue(CombatEncounter, { end: BigInt(0) }),
+ ]),
+ )
+ .map(entity => {
+ const encounter = getComponentValue(CombatEncounter, entity);
+ if (!encounter) return null;
+
+ return {
+ attackers: encounter.attackers as Entity[],
+ currentTurn: encounter.currentTurn.toString(),
+ defenders: encounter.defenders as Entity[],
+ encounterId: entity,
+ encounterType: encounter.encounterType,
+ end: encounter.end.toString(),
+ maxTurns: encounter.maxTurns.toString(),
+ start: encounter.start.toString(),
+ };
+ })
+ .filter(
+ encounter =>
+ character &&
+ (encounter?.attackers.includes(character.characterId) ||
+ encounter?.defenders.includes(character.characterId)),
+ )[0] ?? null;
+
const onMove = useCallback(
async (direction: 'up' | 'down' | 'left' | 'right') => {
try {
+ if (currentBattle) return;
setIsMoving(true);
if (!delegatorAddress) {
@@ -367,9 +403,19 @@ export const MapNavigationProvider = ({
setIsMoving(false);
}
},
- [character, delegatorAddress, move, position, renderError],
+ [character, currentBattle, delegatorAddress, move, position, renderError],
);
+ const monsterOponent = useMemo(() => {
+ if (!currentBattle) return null;
+
+ return (
+ monsters.find(monster =>
+ currentBattle.defenders.includes(monster.monsterId),
+ ) ?? null
+ );
+ }, [currentBattle, monsters]);
+
useEffect(() => {
if (pathname !== GAME_BOARD_PATH) return;
@@ -412,9 +458,11 @@ export const MapNavigationProvider = ({
return (
Date: Sun, 28 Jul 2024 16:22:34 -0600
Subject: [PATCH 4/5] Prevent attack flicker on reload
---
.../client/src/components/ActionsPanel.tsx | 130 +---------------
.../src/components/TileDetailsPanel.tsx | 23 ++-
.../src/contexts/MapNavigationContext.tsx | 139 +++++++++++++++++-
3 files changed, 156 insertions(+), 136 deletions(-)
diff --git a/packages/client/src/components/ActionsPanel.tsx b/packages/client/src/components/ActionsPanel.tsx
index 453be0fde..eeb70aa4f 100644
--- a/packages/client/src/components/ActionsPanel.tsx
+++ b/packages/client/src/components/ActionsPanel.tsx
@@ -1,23 +1,11 @@
import { Button, HStack, Stack, Text, VStack } from '@chakra-ui/react';
-import { useEntityQuery } from '@latticexyz/react';
-import {
- getComponentValueStrict,
- Has,
- HasValue,
- runQuery,
-} from '@latticexyz/recs';
-import { decodeEntity } from '@latticexyz/store-sync/recs';
-import { useCallback, useMemo, useState } from 'react';
+import { useMemo } from 'react';
import { Link } from 'react-router-dom';
// eslint-disable-next-line import/no-named-as-default
import Typist from 'react-typist';
-import { formatUnits } from 'viem';
import { useCharacter } from '../contexts/CharacterContext';
import { useMapNavigation } from '../contexts/MapNavigationContext';
-import { useMUD } from '../contexts/MUDContext';
-import { useToast } from '../hooks/useToast';
-import { ActionType, type BattleActionOutcome } from '../utils/types';
// enum ActionEvents {
// Attack = 'attack',
@@ -62,69 +50,23 @@ import { ActionType, type BattleActionOutcome } from '../utils/types';
// ];
export const ActionsPanel = (): JSX.Element => {
- const { renderError } = useToast();
- const {
- components: { ActionOutcome, Actions },
- delegatorAddress,
- systemCalls: { endTurn },
- } = useMUD();
const {
isRefreshing: isRefreshingCharacter,
character,
equippedItems,
- refreshCharacter,
} = useCharacter();
const {
+ battleActionOutcomes,
currentBattle,
+ isAttacking,
isRefreshing: isRefreshingMap,
isSpawned,
monsterOponent,
monsters,
+ onAttack,
position,
} = useMapNavigation();
- const [isAttacking, setIsAttacking] = useState(false);
-
- const battleActionOutcomes = useEntityQuery([
- Has(ActionOutcome),
- HasValue(ActionOutcome, { attackerId: character?.characterId }),
- ])
- .map(entity => {
- const _actionOutcome = getComponentValueStrict(ActionOutcome, entity);
-
- const { encounterId, currentTurn, actionNumber } = decodeEntity(
- {
- encounterId: 'bytes32',
- currentTurn: 'uint256',
- actionNumber: 'uint256',
- },
- entity,
- );
-
- return {
- attackerDamageDelt: formatUnits(
- _actionOutcome.attackerDamageDelt,
- 5,
- ).toString(),
- attackerDied: _actionOutcome.attackerDied,
- attackerId: _actionOutcome.attackerId.toString(),
- actionId: _actionOutcome.actionId.toString(),
- actionNumber: actionNumber.toString(),
- blockNumber: _actionOutcome.blockNumber.toString(),
- crit: _actionOutcome.crit,
- currentTurn: currentTurn.toString(),
- defenderDamageDelt: _actionOutcome.defenderDamageDelt.toString(),
- defenderDied: _actionOutcome.defenderDied,
- defenderId: _actionOutcome.defenderId.toString(),
- encounterId: encounterId.toString(),
- hit: _actionOutcome.hit,
- miss: _actionOutcome.miss,
- timestamp: _actionOutcome.timestamp.toString(),
- weaponId: _actionOutcome.weaponId.toString(),
- } as BattleActionOutcome;
- })
- .filter(action => action.encounterId === currentBattle?.encounterId);
-
const actionText = useMemo(() => {
if (isRefreshingCharacter || isRefreshingMap) return '';
@@ -203,70 +145,6 @@ export const ActionsPanel = (): JSX.Element => {
return '';
}, [isRefreshingCharacter, isRefreshingMap, isSpawned, monsters, position]);
- const onAttack = useCallback(
- async (itemId: string) => {
- try {
- setIsAttacking(true);
-
- if (!delegatorAddress) {
- throw new Error('Missing delegation.');
- }
-
- if (!character) {
- throw new Error('Character not found.');
- }
-
- if (!currentBattle) {
- throw new Error('Battle not found.');
- }
-
- if (!monsterOponent) {
- throw new Error('Monster not found.');
- }
-
- const basicAttackId = Array.from(
- runQuery([
- Has(Actions),
- HasValue(Actions, { actionType: ActionType.PhysicalAttack }),
- ]),
- )[0];
-
- if (!basicAttackId) {
- throw new Error('Basic attack not found.');
- }
-
- const { error, success } = await endTurn(
- currentBattle.encounterId,
- character.characterId,
- monsterOponent.monsterId,
- basicAttackId,
- itemId,
- currentBattle.currentTurn,
- );
-
- if (error && !success) {
- throw new Error(error);
- }
-
- await refreshCharacter();
- } catch (e) {
- renderError((e as Error)?.message ?? 'Failed to attack.', e);
- } finally {
- setIsAttacking(false);
- }
- },
- [
- Actions,
- character,
- currentBattle,
- delegatorAddress,
- endTurn,
- monsterOponent,
- refreshCharacter,
- renderError,
- ],
- );
-
return (
<>
{currentBattle && equippedItems && monsterOponent && (
diff --git a/packages/client/src/components/TileDetailsPanel.tsx b/packages/client/src/components/TileDetailsPanel.tsx
index bf56ac8a1..7722facad 100644
--- a/packages/client/src/components/TileDetailsPanel.tsx
+++ b/packages/client/src/components/TileDetailsPanel.tsx
@@ -35,6 +35,7 @@ export const TileDetailsPanel = (): JSX.Element => {
} = useMUD();
const { character } = useCharacter();
const {
+ battleActionOutcomes,
currentBattle,
isRefreshing,
monsterOponent,
@@ -46,14 +47,30 @@ export const TileDetailsPanel = (): JSX.Element => {
const [isMonsterHit, setIsMonsterHit] = useState(false);
useEffect(() => {
- if (!monsterOponent) return;
- if (monsterOponent.currentHp === monsterOponent.baseHp) return;
+ if (!(battleActionOutcomes[0] && currentBattle)) return;
+
+ const currentBattleTurnKey = 'current-battle-turn';
+ const currentBattleTurn = localStorage.getItem(currentBattleTurnKey);
+
+ if (currentBattleTurn) {
+ if (currentBattleTurn === currentBattle.currentTurn) {
+ return;
+ }
+ }
+
+ if (
+ battleActionOutcomes[battleActionOutcomes.length - 1]
+ .attackerDamageDelt === '0'
+ )
+ return;
setIsMonsterHit(true);
setTimeout(() => {
setIsMonsterHit(false);
}, 700);
- }, [monsterOponent, monsterOponent?.baseHp, monsterOponent?.currentHp]);
+
+ localStorage.setItem(currentBattleTurnKey, currentBattle.currentTurn);
+ }, [battleActionOutcomes, currentBattle]);
const onInitiateCombat = useCallback(
async (monster: Monster) => {
diff --git a/packages/client/src/contexts/MapNavigationContext.tsx b/packages/client/src/contexts/MapNavigationContext.tsx
index bbaa8d2ec..a43910961 100644
--- a/packages/client/src/contexts/MapNavigationContext.tsx
+++ b/packages/client/src/contexts/MapNavigationContext.tsx
@@ -6,8 +6,9 @@ import {
Has,
HasValue,
Not,
+ runQuery,
} from '@latticexyz/recs';
-import { encodeEntity } from '@latticexyz/store-sync/recs';
+import { decodeEntity, encodeEntity } from '@latticexyz/store-sync/recs';
import {
createContext,
ReactNode,
@@ -21,6 +22,7 @@ import { useLocation } from 'react-router-dom';
import {
bytesToHex,
formatEther,
+ formatUnits,
hexToBytes,
hexToString,
zeroHash,
@@ -29,17 +31,26 @@ import {
import { useToast } from '../hooks/useToast';
import { GAME_BOARD_PATH } from '../Routes';
import { fetchMetadataFromUri, uriToHttp } from '../utils/helpers';
-import type { Character, CombatDetails, Monster } from '../utils/types';
+import {
+ ActionType,
+ type BattleActionOutcome,
+ type Character,
+ type CombatDetails,
+ type Monster,
+} from '../utils/types';
import { useCharacter } from './CharacterContext';
import { useMUD } from './MUDContext';
type MapNavigationContextType = {
+ battleActionOutcomes: BattleActionOutcome[];
currentBattle: CombatDetails | null;
+ isAttacking: boolean;
isRefreshing: boolean;
isSpawned: boolean;
isSpawning: boolean;
monsterOponent: Monster | null;
monsters: Monster[];
+ onAttack: (itemId: string) => void;
onMove: (direction: 'up' | 'down' | 'left' | 'right') => void;
onSpawn: () => void;
otherPlayers: Character[];
@@ -47,12 +58,15 @@ type MapNavigationContextType = {
};
const MapNavigationContext = createContext({
+ battleActionOutcomes: [],
currentBattle: null,
+ isAttacking: false,
isRefreshing: false,
isSpawned: false,
isSpawning: false,
monsterOponent: null,
monsters: [],
+ onAttack: () => {},
onMove: () => {},
onSpawn: () => {},
otherPlayers: [],
@@ -70,6 +84,8 @@ export const MapNavigationProvider = ({
const { renderError, renderSuccess } = useToast();
const {
components: {
+ ActionOutcome,
+ Actions,
Characters,
CharactersTokenURI,
CombatEncounter,
@@ -82,17 +98,19 @@ export const MapNavigationProvider = ({
},
delegatorAddress,
network: { publicClient, worldContract },
- systemCalls: { move, spawn },
+ systemCalls: { endTurn, move, spawn },
} = useMUD();
- const { character } = useCharacter();
-
- const [otherPlayers, setOtherPlayers] = useState([]);
- const [monsters, setMonsters] = useState([]);
+ const { character, refreshCharacter } = useCharacter();
const [isSpawning, setIsSpawning] = useState(false);
const [isMoving, setIsMoving] = useState(false);
const [isFetchingEntities, setIsFetchingEntities] = useState(true);
+ const [otherPlayers, setOtherPlayers] = useState([]);
+ const [monsters, setMonsters] = useState([]);
+
+ const [isAttacking, setIsAttacking] = useState(false);
+
const position = useComponentValue(
Position,
encodeEntity(
@@ -455,15 +473,122 @@ export const MapNavigationProvider = ({
return () => window.removeEventListener('keydown', listener);
}, [onMove, pathname]);
+ const onAttack = useCallback(
+ async (itemId: string) => {
+ try {
+ setIsAttacking(true);
+
+ if (!delegatorAddress) {
+ throw new Error('Missing delegation.');
+ }
+
+ if (!character) {
+ throw new Error('Character not found.');
+ }
+
+ if (!currentBattle) {
+ throw new Error('Battle not found.');
+ }
+
+ if (!monsterOponent) {
+ throw new Error('Monster not found.');
+ }
+
+ const basicAttackId = Array.from(
+ runQuery([
+ Has(Actions),
+ HasValue(Actions, { actionType: ActionType.PhysicalAttack }),
+ ]),
+ )[0];
+
+ if (!basicAttackId) {
+ throw new Error('Basic attack not found.');
+ }
+
+ const { error, success } = await endTurn(
+ currentBattle.encounterId,
+ character.characterId,
+ monsterOponent.monsterId,
+ basicAttackId,
+ itemId,
+ currentBattle.currentTurn,
+ );
+
+ if (error && !success) {
+ throw new Error(error);
+ }
+
+ await refreshCharacter();
+ } catch (e) {
+ renderError((e as Error)?.message ?? 'Failed to attack.', e);
+ } finally {
+ setIsAttacking(false);
+ }
+ },
+ [
+ Actions,
+ character,
+ currentBattle,
+ delegatorAddress,
+ endTurn,
+ monsterOponent,
+ refreshCharacter,
+ renderError,
+ ],
+ );
+
+ const battleActionOutcomes = useEntityQuery([
+ Has(ActionOutcome),
+ HasValue(ActionOutcome, { attackerId: character?.characterId }),
+ ])
+ .map(entity => {
+ const _actionOutcome = getComponentValueStrict(ActionOutcome, entity);
+
+ const { encounterId, currentTurn, actionNumber } = decodeEntity(
+ {
+ encounterId: 'bytes32',
+ currentTurn: 'uint256',
+ actionNumber: 'uint256',
+ },
+ entity,
+ );
+
+ return {
+ attackerDamageDelt: formatUnits(
+ _actionOutcome.attackerDamageDelt,
+ 5,
+ ).toString(),
+ attackerDied: _actionOutcome.attackerDied,
+ attackerId: _actionOutcome.attackerId.toString(),
+ actionId: _actionOutcome.actionId.toString(),
+ actionNumber: actionNumber.toString(),
+ blockNumber: _actionOutcome.blockNumber.toString(),
+ crit: _actionOutcome.crit,
+ currentTurn: currentTurn.toString(),
+ defenderDamageDelt: _actionOutcome.defenderDamageDelt.toString(),
+ defenderDied: _actionOutcome.defenderDied,
+ defenderId: _actionOutcome.defenderId.toString(),
+ encounterId: encounterId.toString(),
+ hit: _actionOutcome.hit,
+ miss: _actionOutcome.miss,
+ timestamp: _actionOutcome.timestamp.toString(),
+ weaponId: _actionOutcome.weaponId.toString(),
+ } as BattleActionOutcome;
+ })
+ .filter(action => action.encounterId === currentBattle?.encounterId);
+
return (
Date: Sun, 28 Jul 2024 16:24:10 -0600
Subject: [PATCH 5/5] Temporarily bring back DevTools for production
---
packages/client/src/index.tsx | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/packages/client/src/index.tsx b/packages/client/src/index.tsx
index e4ea6f209..6be302c5b 100644
--- a/packages/client/src/index.tsx
+++ b/packages/client/src/index.tsx
@@ -33,7 +33,9 @@ setup().then(async result => {
- {import.meta.env.DEV && }
+ {/* TODO: Bring back dev check before release */}
+ {/* {import.meta.env.DEV && } */}
+
,