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
2 changes: 1 addition & 1 deletion packages/client/src/Routes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ const AppRoutes: React.FC = () => {
<Route path={HOME_PATH} element={<Welcome />} />
<Route path={CHARACTER_CREATION_PATH} element={<CharacterCreation />} />
<Route path={GAME_BOARD_PATH} element={<GameBoard />} />
<Route path="/characters/:characterId" element={<CharacterPage />} />
<Route path="/characters/:id" element={<CharacterPage />} />
<Route path={LEADERBOARD_PATH} element={<Leaderboard />} />
</Routes>
);
Expand Down
189 changes: 155 additions & 34 deletions packages/client/src/components/ActionsPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,12 @@ import {
Button,
Divider,
HStack,
Progress,
Stack,
Text,
VStack,
} from '@chakra-ui/react';
import { useEffect, useMemo, useRef } from 'react';
import { useEffect, useMemo, useRef, useState } from 'react';
import { Link } from 'react-router-dom';
// eslint-disable-next-line import/no-named-as-default
import Typist from 'react-typist';
Expand All @@ -16,25 +17,28 @@ import { useBattle } from '../contexts/BattleContext';
import { useCharacter } from '../contexts/CharacterContext';
import { useMap } from '../contexts/MapContext';
import { useMovement } from '../contexts/MovementContext';
import { EncounterType } from '../utils/types';

export const ActionsPanel = (): JSX.Element => {
const {
isRefreshing: isRefreshingCharacter,
character,
equippedWeapons,
} = useCharacter();
const { aliveMonsters, isSpawned, position } = useMap();
const { isSpawned, monstersOnTile, position } = useMap();
const {
actionOutcomes,
attackingItemId,
currentBattle,
lastestBattleOutcome,
monsterOponent,
onAttack,
onContinueToBattleOutcome,
opponent,
} = useBattle();
const { isRefreshing: isRefreshingMap } = useMovement();

const [turnTimeLeft, setTurnTimeLeft] = useState<number>(32);

const parentDivRef = useRef<HTMLDivElement>(null);

useEffect(() => {
Expand Down Expand Up @@ -90,7 +94,7 @@ export const ActionsPanel = (): JSX.Element => {
);
}

if ((position.x !== 0 || position.y !== 0) && aliveMonsters.length === 0) {
if ((position.x !== 0 || position.y !== 0) && monstersOnTile.length === 0) {
return (
<Typist
avgTypingDelay={10}
Expand All @@ -109,7 +113,7 @@ export const ActionsPanel = (): JSX.Element => {
);
}

if ((position.x !== 0 || position.y !== 0) && aliveMonsters.length > 0) {
if ((position.x !== 0 || position.y !== 0) && monstersOnTile.length > 0) {
return (
<Typist
avgTypingDelay={10}
Expand All @@ -129,36 +133,143 @@ export const ActionsPanel = (): JSX.Element => {

return '';
}, [
aliveMonsters,
isRefreshingCharacter,
isRefreshingMap,
isSpawned,
monstersOnTile,
position,
]);

const userTurn = useMemo(() => {
if (!(character && currentBattle)) return false;

if (currentBattle.encounterType === EncounterType.PvE) {
return true;
}

const attackersTurn = Number(currentBattle.currentTurn) % 2 === 1;

if (attackersTurn && currentBattle.attackers.includes(character?.id)) {
return true;
}

if (!attackersTurn && currentBattle.defenders.includes(character?.id)) {
return true;
}

return false;
}, [character, currentBattle]);

const turnEndTime = useMemo(() => {
if (!currentBattle) return 0;

const _turnEndTime =
(BigInt(currentBattle.currentTurnTimer) + BigInt(32)) * BigInt(1000);

return Number(_turnEndTime);
}, [currentBattle]);

useEffect(() => {
if (turnEndTime - Date.now() < 0) {
setTurnTimeLeft(0);
} else {
setTurnTimeLeft(Math.floor((turnEndTime - Date.now()) / 1000));
}

const interval = setInterval(() => {
if (turnEndTime - Date.now() < 0) {
setTurnTimeLeft(0);
return;
}
setTurnTimeLeft(prev => prev - 1);
}, 1000);

return () => clearInterval(interval);
}, [turnEndTime, turnTimeLeft]);

const canAttack = useMemo(() => {
if (!currentBattle) return false;

if (currentBattle.encounterType === EncounterType.PvE) {
return true;
}

if (userTurn) {
return true;
}

if (turnTimeLeft === 0) {
return true;
}

return false;
}, [currentBattle, userTurn, turnTimeLeft]);

return (
<Box maxH="100%" overflowY="auto" pb={4} ref={parentDivRef}>
{!battleOver && currentBattle && equippedWeapons && monsterOponent && (
{!battleOver && currentBattle && equippedWeapons && opponent && (
<VStack bgColor="white" position="sticky" spacing={0} top={0} w="100%">
<Text p={{ base: 2, lg: 4 }} size="xs" textAlign="center">
Choose your move:
</Text>
{equippedWeapons.length === 0 && (
<Text color="red" fontWeight={700} p={{ base: 2, lg: 4 }}>
You have no equipped items. In order to attack, you must go to
your{' '}
<Text
as={Link}
color="blue"
to={`/characters/${character?.characterId}`}
_hover={{ textDecoration: 'underline' }}
>
character page
</Text>{' '}
and equip at least 1 item.
{currentBattle.encounterType === EncounterType.PvE && (
<Text p={{ base: 2, lg: 4 }} size="xs" textAlign="center">
<Text as="span" fontWeight="bold">
Choose your move!
</Text>
</Text>
)}
<HStack spacing={0} w="100%">

{currentBattle.encounterType === EncounterType.PvP && (
<>
{userTurn && (
<Text p={{ base: 2, lg: 4 }} size="xs" textAlign="center">
<Text as="span" fontWeight="bold">
Choose your move!
</Text>{' '}
You have {turnTimeLeft} seconds before your opponent can
attack.
</Text>
)}
{!userTurn && !canAttack && (
<Text p={{ base: 2, lg: 4 }} size="xs" textAlign="center">
It is your opponent&apos;s turn. But you can attack in{' '}
{turnTimeLeft} seconds.
</Text>
)}
{!userTurn && canAttack && (
<Text p={{ base: 2, lg: 4 }} size="xs" textAlign="center">
Your opponent took too long to make a move.{' '}
<Text as="span" fontWeight={700}>
You can now attack!
</Text>
</Text>
)}
{equippedWeapons.length === 0 && (
<Text color="red" fontWeight={700} p={{ base: 2, lg: 4 }}>
You have no equipped items. In order to attack, you must go to
your{' '}
<Text
as={Link}
color="blue"
to={`/characters/${character?.id}`}
_hover={{ textDecoration: 'underline' }}
>
character page
</Text>{' '}
and equip at least 1 item.
</Text>
)}
</>
)}
<HStack position="relative" spacing={0} w="100%">
{currentBattle.encounterType === EncounterType.PvP && (
<Progress
position="absolute"
size="xs"
top={-1}
value={(turnTimeLeft / 32) * 100}
variant="timer"
w="100%"
/>
)}
{equippedWeapons.map((item, index) => (
<Button
borderLeft={index === 0 ? 'none' : '2px'}
Expand All @@ -168,11 +279,21 @@ export const ActionsPanel = (): JSX.Element => {
? 'none'
: '2px'
}
isDisabled={attackingItemId !== null}
isDisabled={attackingItemId !== null || !canAttack}
isLoading={attackingItemId === item.tokenId}
key={`equipped-item-${index}`}
loadingText="Attacking..."
onClick={() => onAttack(item.tokenId)}
onClick={() =>
onAttack(
item.tokenId,
userTurn ||
currentBattle.encounterType === EncounterType.PvE
? currentBattle.currentTurn
: (
BigInt(currentBattle.currentTurn) + BigInt(1)
).toString(),
)
}
variant="outline"
w="100%"
>
Expand All @@ -185,7 +306,7 @@ export const ActionsPanel = (): JSX.Element => {
<Stack p={{ base: 2, lg: 4 }}>
{!currentBattle && actionText}

{monsterOponent &&
{opponent &&
actionOutcomes.map((action, i) => {
if (action.miss) {
return (
Expand All @@ -195,14 +316,14 @@ export const ActionsPanel = (): JSX.Element => {
key={`battle-action-${i}`}
stdTypingDelay={10}
>
{action.attackerId === character?.characterId ? (
{action.attackerId === character?.id ? (
<Text
key={`battle-action-${i}`}
size={{ base: 'xs', sm: 'sm', lg: 'md' }}
>
You missed{' '}
<Text as="span" color="green">
{monsterOponent.name}
{opponent.name}
</Text>
.
</Text>
Expand All @@ -212,7 +333,7 @@ export const ActionsPanel = (): JSX.Element => {
size={{ base: 'xs', sm: 'sm', lg: 'md' }}
>
<Text as="span" color="green">
{monsterOponent.name}
{opponent.name}
</Text>{' '}
missed you.
</Text>
Expand All @@ -230,11 +351,11 @@ export const ActionsPanel = (): JSX.Element => {
key={`battle-action-${i}`}
stdTypingDelay={10}
>
{action.attackerId === character?.characterId ? (
{action.attackerId === character?.id ? (
<Text size={{ base: 'xs', sm: 'sm', lg: 'md' }}>
{critText}You attacked{' '}
<Text as="span" color="green">
{monsterOponent?.name}
{opponent?.name}
</Text>{' '}
for{' '}
<Text as="span" color="red">
Expand All @@ -246,7 +367,7 @@ export const ActionsPanel = (): JSX.Element => {
<Text size={{ base: 'xs', sm: 'sm', lg: 'md' }}>
{critText}
<Text as="span" color="green">
{monsterOponent?.name}
{opponent?.name}
</Text>{' '}
attacked you for{' '}
<Text as="span" color="red">
Expand All @@ -272,7 +393,7 @@ export const ActionsPanel = (): JSX.Element => {
size={{ base: 'xs', sm: 'sm', lg: 'md' }}
textAlign="center"
>
{lastestBattleOutcome?.winner === character?.characterId
{lastestBattleOutcome?.winner === character?.id
? 'You won!'
: 'You lost...'}
</Text>
Expand Down
Loading