Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
161 changes: 157 additions & 4 deletions packages/client/src/components/ActionsPanel.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,15 @@
import { Stack, Text } from '@chakra-ui/react';
import { Button, HStack, Stack, Text, VStack } from '@chakra-ui/react';
import { Has, HasValue, runQuery } from '@latticexyz/recs';
import { useCallback, useMemo, useState } from 'react';
import { Link } from 'react-router-dom';

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';
import { ActionType } from '../utils/types';
import { HealthBar } from './HealthBar';

// enum ActionEvents {
// Attack = 'attack',
Expand Down Expand Up @@ -43,12 +54,154 @@ import { Stack, Text } from '@chakra-ui/react';
// ];

export const ActionsPanel = (): JSX.Element => {
const { renderError } = useToast();
const {
burnerBalance,
components: { Actions },
delegatorAddress,
systemCalls: { endTurn },
} = useMUD();
const { character, equippedItems, refreshCharacter } = useCharacter();
const { currentBattle, monster } = useCombat();
const { isSpawned } = useMapNavigation();

const [isAttacking, setIsAttacking] = useState(false);

const actionText = useMemo(() => {
if (!isSpawned) {
return 'You must spawn on the map to start battling.';
}

if (currentBattle && monster) {
return `You are currently in a battle with a ${monster.name}.`;
}

return 'To initiate a battle, move into a new tile and click on a monster.';
}, [currentBattle, isSpawned, monster]);

const onAttack = useCallback(
async (itemId: string) => {
try {
setIsAttacking(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.');
}

if (!currentBattle) {
throw new Error('Battle not found.');
}

if (!monster) {
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,
monster.monsterId,
basicAttackId,
itemId,
currentBattle.currentTurn,
);

if (error && !success) {
throw new Error(error);
}

await refreshCharacter();
} catch (e) {
renderError('Failed to attack.', e);
} finally {
setIsAttacking(false);
}
},
[
Actions,
burnerBalance,
character,
currentBattle,
delegatorAddress,
endTurn,
monster,
refreshCharacter,
renderError,
],
);

return (
<Stack spacing={8}>
<Stack>
<Text size={{ base: 'xs', sm: 'sm', lg: 'md' }}>
You must spawn on the map to start battling.
</Text>
<Text size={{ base: 'xs', sm: 'sm', lg: 'md' }}>{actionText}</Text>
{currentBattle && equippedItems && monster && (
<HealthBar
baseHp={monster.baseHp}
currentHp={monster.currentHp}
mt={4}
w="80%"
/>
)}
{currentBattle && equippedItems && monster && (
<HStack justify="center">
{equippedItems.length === 0 && (
<Text color="red" fontWeight={700}>
You have no equipped items. In order to attack, you must go to
your{' '}
<Text
as={Link}
color="green"
to={`/characters/${character?.characterId}`}
_hover={{ textDecoration: 'underline' }}
>
character page
</Text>{' '}
and equip at least 1 item.
</Text>
)}
{equippedItems.map((item, index) => (
<Button
key={`equipped-item-${index}`}
isLoading={isAttacking}
loadingText="Attacking..."
mt={8}
onClick={() => onAttack(item.tokenId)}
>
Attack with {item.name}
</Button>
))}
</HStack>
)}
{currentBattle && equippedItems && monster && (
<VStack mt={4}>
<Text fontWeight={700}>MONSTER STATS:</Text>
<HStack>
<Text>Attack: {monster.agility}</Text>
<Text>Defense: {monster.intelligence}</Text>
<Text>Level: {monster.level}</Text>
</HStack>
</VStack>
)}
</Stack>
{/* <Stack>
{BATTLE_EVENTS.map((event, i) => (
Expand Down
10 changes: 7 additions & 3 deletions packages/client/src/components/Character/Stats.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@ import { HStack, Text, VStack } from '@chakra-ui/react';

export const Stats = ({
agility,
baseHitPoints,
baseHp,
currentHp,
intelligence,
strength,
}: {
agility: string;
baseHitPoints: string;
baseHp: string;
currentHp: string;
intelligence: string;
strength: string;
}): JSX.Element => {
Expand All @@ -27,7 +29,9 @@ export const Stats = ({
<VStack w="100%">
<HStack justify="space-between" w="100%">
<Text size="lg">HP - Hit</Text>
<Text size="lg">{baseHitPoints}</Text>
<Text size="lg">
{currentHp}/{baseHp}
</Text>
</HStack>

<HStack justify="space-between" w="100%">
Expand Down
10 changes: 5 additions & 5 deletions packages/client/src/components/DelegationButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,26 +28,26 @@ export const DelegationButton = ({
const onSetupDelegation = useCallback(async () => {
try {
if (!setupDelegation) {
throw new Error('Delegation setup function not available');
throw new Error('Delegation setup function not available.');
}

if (!network) {
throw new Error('Network not available');
throw new Error('Network not available.');
}

setIsDelegating(true);
await setupDelegation(network, externalWalletClient, burnerAddress);

renderSuccess('Delegation successful');
renderSuccess('Delegation successful.');

if (onClose) {
onClose();
}

getBurner();
navigate(CHARACTER_CREATION_PATH);
} catch (error) {
renderError(error, 'Failed to delegate');
} catch (e) {
renderError('Failed to delegate.', e);
} finally {
setIsDelegating(false);
}
Expand Down
4 changes: 4 additions & 0 deletions packages/client/src/components/DevTools.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import mudConfig from 'contracts/mud.config';
import characterSystemAbi from 'contracts/out/CharacterSystem.sol/CharacterSystem.abi.json';
import combatSystemAbi from 'contracts/out/CombatSystem.sol/CombatSystem.abi.json';
import equipmentSystemAbi from 'contracts/out/EquipmentSystem.sol/EquipmentSystem.abi.json';
import mapSystemAbi from 'contracts/out/MapSystem.sol/MapSystem.abi.json';
import { useEffect, useMemo } from 'react';

Expand All @@ -13,6 +15,8 @@ export function DevTools(): null {
() => [
...network.worldContract.abi,
...characterSystemAbi,
...combatSystemAbi,
...equipmentSystemAbi,
...mapSystemAbi,
],
[network.worldContract.abi],
Expand Down
36 changes: 36 additions & 0 deletions packages/client/src/components/HealthBar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { Box, Flex, StackProps, Text, VStack } from '@chakra-ui/react';

export const HealthBar = ({
currentHp,
baseHp,
...stackProps
}: {
currentHp: string;
baseHp: string;
} & StackProps): JSX.Element => {
const health = (parseInt(currentHp) / parseInt(baseHp)) * 100;

const barColor = health > 50 ? 'green' : health > 15 ? 'yellow' : 'red';

return (
<VStack alignItems="end" spacing={0.5} {...stackProps}>
<Flex border="2px solid black" width="100%" height="24px">
<Text
bgColor="black"
color="white"
fontSize="sm"
fontWeight={700}
px={2}
>
HP
</Text>
<Box borderLeft="2px solid black" h="100%" position="relative" w="100%">
<Box bgColor={barColor} h="100%" w={`${health}%`} />
</Box>
</Flex>
<Text fontSize="xs" fontWeight={700}>
{currentHp} / {baseHp}
</Text>
</VStack>
);
};
109 changes: 109 additions & 0 deletions packages/client/src/components/InitiateCombatModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import {
Button,
Modal,
ModalBody,
ModalCloseButton,
ModalContent,
ModalFooter,
ModalHeader,
ModalOverlay,
} from '@chakra-ui/react';
import { useCallback, useState } from 'react';

import { useCharacter } from '../contexts/CharacterContext';
import { useMUD } from '../contexts/MUDContext';
import { useToast } from '../hooks/useToast';
import { EncounterType, type Monster } from '../utils/types';

type InitiateCombatModalProps = Monster & {
isOpen: boolean;
onClose: () => void;
};

export const InitiateCombatModal: React.FC<InitiateCombatModalProps> = ({
isOpen,
onClose,
...monster
}): JSX.Element => {
const { renderError, renderSuccess } = useToast();
const {
burnerBalance,
delegatorAddress,
systemCalls: { createMatch },
} = useMUD();
const { character } = useCharacter();

const [isInitiating, setIsInitiating] = useState(false);

const onInitiateCombat = useCallback(async () => {
try {
setIsInitiating(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 { error, success } = await createMatch(
EncounterType.PvE,
[character.characterId],
[monster.monsterId],
);

if (error && !success) {
throw new Error(error);
}

renderSuccess(`Battle has begun!`);
onClose();
} catch (e) {
renderError('Failed to initiate battle.', e);
} finally {
setIsInitiating(false);
}
}, [
burnerBalance,
character,
createMatch,
delegatorAddress,
monster,
onClose,
renderError,
renderSuccess,
]);

return (
<Modal isOpen={isOpen} onClose={onClose}>
<ModalOverlay />
<ModalContent>
<ModalHeader>Initiate Battle</ModalHeader>
<ModalCloseButton />
<ModalBody padding={4}>
Are you sure you want to initiate a battle with {monster.name}?
</ModalBody>
<ModalFooter>
<Button
isLoading={isInitiating}
loadingText="Initiating..."
mr={3}
onClick={onInitiateCombat}
>
Yes
</Button>
<Button isDisabled={isInitiating} onClick={onClose} variant="ghost">
No
</Button>
</ModalFooter>
</ModalContent>
</Modal>
);
};
Loading