Skip to content

Commit

Permalink
[FE] 순간이동 기능 구현 및 기존 버그들 수정 (#71)
Browse files Browse the repository at this point in the history
* refactor: Dice 컴포넌트 내부 moveToken 함수 커스텀훅으로 분리

- 순간이동 기능 구현시 재사용을 위해서 커스텀훅으로 분리

* refactor: 플레이어의 이동종료 여부 상태 리팩터링

- 기존 CenterArea 컴포넌트 내부 state인 isMoveFinished를 GameInfo에서 전역적으로 관리하도록 변경
- 기존 방식에는 주사위 더블일때 버그가 있어서 수정함
- 안쓰는 nextPlayerStatus 삭제

* fix: 리듀서에서 isMoveFinished, hasEscaped 관련 수정

- 기존 방식으로는 보석금 지불 탈출 시 버그가 있어서 수정

* feat: CenterArea 컴포넌트에서 handleTeleport 함수 구현

- 텔레포트칸에서 유저 위치를 이동시킬 때 사용할 함수 구현
- 인자로 넘길 targetCell을 선택할수 있게 하는 기능 추가예정

* feat: 순간이동 구현 중간 커밋

- 모두에게 같은 화면을 보여주지 못하는 문제 해결 필요

* feat: 순간이동 기능 구현

- 서버에서 teleport 타입 메세지 받는것으로 수정해서 구현
- 텔레포트시 선택된 칸 색깔 강조

* fix: 순간이동 칸으로 다시 순간이동 못하게 수정
  • Loading branch information
silvertae authored Nov 2, 2023
1 parent e007e75 commit 94752a8
Show file tree
Hide file tree
Showing 10 changed files with 345 additions and 149 deletions.
58 changes: 45 additions & 13 deletions fe/src/components/GameBoard/Cell.tsx
Original file line number Diff line number Diff line change
@@ -1,41 +1,73 @@
import { cellImageMap } from '@assets/images';
import { PlayerStatusType } from '@store/reducer/type';
import { addCommasToNumber } from '@utils/index';
import { styled } from 'styled-components';

type Cellprops = {
type CellType = {
theme?: string;
name: string;
logo: string;
location: number;
};

type Cellprops = {
cell: CellType;
price?: number;
playerStatus: PlayerStatusType;
targetLocation: number | null;
selectTargetLocation: (location: number) => void;
};

export default function Cell({ theme, name, logo, price }: Cellprops) {
const addCommasToNumber = (number: number): string => {
return `${number.toLocaleString('ko')}`;
};
export default function Cell({
cell,
price,
playerStatus,
targetLocation,
selectTargetLocation,
}: Cellprops) {
const isSelected = targetLocation === cell.location;

return (
<Container>
{theme && (
<Container
$status={playerStatus}
$isSelected={isSelected}
onClick={() => {
if (playerStatus !== 'teleport') return;
if (cell.location === 18) {
alert('이동할 수 없는 위치입니다.');
return;
}
selectTargetLocation(cell.location);
return;
}}
>
{cell.theme && (
<Header>
<Logo src={cellImageMap[logo]} />
<Name>{name}</Name>
<Logo src={cellImageMap[cell.logo]} />
<Name>{cell.name}</Name>
</Header>
)}
<Content>
{!theme && <CellImg src={cellImageMap[logo]} />}
{!cell.theme && <CellImg src={cellImageMap[cell.logo]} />}
{price && <span>{addCommasToNumber(price)}</span>}
</Content>
</Container>
);
}

const Container = styled.div`
const Container = styled.div<{
$status: PlayerStatusType;
$isSelected: boolean;
}>`
width: 6rem;
height: 6rem;
display: flex;
flex-direction: column;
border: 1px solid;
border-color: ${({ theme: { color } }) => color.accentText};
border-width: 1px;
border-style: solid;
border-color: ${({ theme }) => theme.color.accentText};
background-color: ${({ theme, $isSelected }) =>
$isSelected ? theme.color.accentTertiary : theme.color.accentPrimary};
`;

const Header = styled.div`
Expand Down
99 changes: 80 additions & 19 deletions fe/src/components/GameBoard/CenterArea.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,31 @@
import useGetSocketUrl from '@hooks/useGetSocketUrl';
import useHover from '@hooks/useHover';
import useMoveToken from '@hooks/useMoveToken';
import { usePlayerIdValue } from '@store/index';
import { useGameInfoValue, usePlayersValue } from '@store/reducer';
import { useCallback, useEffect, useState } from 'react';
import {
useGameInfoValue,
usePlayersValue,
useResetTeleportLocation,
} from '@store/reducer';
import { PlayerStatusType } from '@store/reducer/type';
import { useEffect } from 'react';
import { useParams } from 'react-router-dom';
import useWebSocket from 'react-use-websocket';
import { styled } from 'styled-components';
import Dice from './Dice';
import Roulette from './Roulette';

export default function CenterArea() {
const [isMoveFinished, setIsMoveFinished] = useState(false);
type CenterAreaProps = {
currentStatus: PlayerStatusType;
targetLocation: number | null;
resetTargetLocation: () => void;
};

export default function CenterArea({
currentStatus,
targetLocation,
resetTargetLocation,
}: CenterAreaProps) {
const { hoverRef: bailRef, isHover: isBailBtnHover } =
useHover<HTMLButtonElement>();
const { hoverRef: escapeRef, isHover: isEscapeBtnHover } =
Expand All @@ -20,19 +35,26 @@ export default function CenterArea() {
const gameInfo = useGameInfoValue();
const playerId = usePlayerIdValue();
const socketUrl = useGetSocketUrl();
const moveToken = useMoveToken();
const resetTeleportLocation = useResetTeleportLocation();
const { sendJsonMessage } = useWebSocket(socketUrl, {
share: true,
});

const eventTime = gameInfo.currentPlayerId === null;
const isMyTurn = playerId === gameInfo.currentPlayerId;
const currentPlayerLocation = players.find(
const eventTime = gameInfo.currentPlayerId === null;
const isPrison = currentStatus === 'prison';
const isTeleport = currentStatus === 'teleport';
const isMoveFinished = gameInfo.isMoveFinished;

const defaultStart =
isMyTurn && !eventTime && !isPrison && !isTeleport && !isMoveFinished;
const prisonStart = isMyTurn && !eventTime && isPrison && !isMoveFinished;
const teleportStart = isMyTurn && !eventTime && isTeleport && !isMoveFinished;

const currentPlayerInfo = players.find(
(player) => player.playerId === gameInfo.currentPlayerId
)?.location;
const isPrison = currentPlayerLocation === 6;
const defaultStart = !eventTime && isMyTurn && !isPrison && !isMoveFinished;
const prisonStart = !eventTime && isMyTurn && isPrison && !isMoveFinished;
// TODO: teleport 구현 필요
);

useEffect(() => {
if (!eventTime) return;
Expand All @@ -44,6 +66,22 @@ export default function CenterArea() {
sendJsonMessage(message);
}, [eventTime, gameId, playerId, gameInfo.firstPlayerId, sendJsonMessage]);

const teleportToken = () => {
if (!currentPlayerInfo || !gameInfo.teleportLocation) return;
const cellCount = calculateCellCount(
gameInfo.teleportLocation,
currentPlayerInfo.gameboard.location
);
moveToken(cellCount, currentPlayerInfo.gameboard, 'teleport');
resetTargetLocation();
resetTeleportLocation();
};

useEffect(() => {
if (!gameInfo.teleportLocation) return;
teleportToken();
}, [gameInfo.teleportLocation]);

const throwDice = () => {
const message = {
type: 'dice',
Expand All @@ -54,7 +92,6 @@ export default function CenterArea() {
};

const endTurn = () => {
setIsMoveFinished(false);
const message = {
type: 'endTurn',
gameId,
Expand All @@ -63,13 +100,9 @@ export default function CenterArea() {
sendJsonMessage(message);
};

const handleFinishMove = useCallback(() => {
setIsMoveFinished(true);
}, []);

const handleBail = () => {
const message = {
type: 'expense',
type: 'bail',
gameId,
playerId,
};
Expand All @@ -85,10 +118,29 @@ export default function CenterArea() {
sendJsonMessage(message);
};

const handleTeleport = () => {
if (!targetLocation) {
alert('이동할 칸을 선택해주세요.');
return;
}
const message = {
type: 'teleport',
gameId,
playerId,
location: targetLocation,
};
sendJsonMessage(message);
};

const calculateCellCount = (targetCell: number, currentCell: number) => {
const cellCount = (24 + targetCell - currentCell) % 24;
return cellCount;
};

return (
<Center>
{eventTime && <Roulette />}
{!eventTime && <Dice finishMove={handleFinishMove} />}
{!eventTime && <Dice />}
{defaultStart && (
<>
<Button onClick={() => throwDice()} disabled={isMoveFinished}>
Expand All @@ -99,14 +151,23 @@ export default function CenterArea() {
{prisonStart && (
<Wrapper>
<Button ref={bailRef} onClick={handleBail}>
{/* Memo: 호버시 내부 텍스트가 안 바뀌는 버그 발견 */}
{isBailBtnHover ? '-5,000,000₩' : '보석금 지불'}
</Button>
<Button ref={escapeRef} onClick={handleEscape}>
{isEscapeBtnHover ? '주사위 더블시 탈출' : '굴려서 탈출'}
</Button>
</Wrapper>
)}
{isMoveFinished && <Button onClick={() => endTurn()}>턴종료</Button>}
{teleportStart && (
<>
<div>이동할 칸을 선택한 후 이동하기 버튼을 눌러주세요.</div>
<Button onClick={() => handleTeleport()}>이동하기</Button>
</>
)}
{isMyTurn && isMoveFinished && (
<Button onClick={() => endTurn()}>턴종료</Button>
)}
</Center>
);
}
Expand Down
88 changes: 6 additions & 82 deletions fe/src/components/GameBoard/Dice.tsx
Original file line number Diff line number Diff line change
@@ -1,96 +1,20 @@
import useMoveToken from '@hooks/useMoveToken';
import { useGameInfoValue, usePlayers } from '@store/reducer';
import { GameBoardType } from '@store/reducer/type';
import { delay } from '@utils/index';
import { MutableRefObject, useEffect, useRef, useState } from 'react';
import { useEffect, useRef, useState } from 'react';
import ReactDice, { ReactDiceRef } from 'react-dice-complete';
import { styled } from 'styled-components';
import {
CORNER_CELLS,
TOKEN_TRANSITION_DELAY,
changeDirection,
directions,
} from './constants';

type DiceProps = {
finishMove: () => void;
};

export default function Dice({ finishMove }: DiceProps) {
export default function Dice() {
const [diceValue, setDiceValue] = useState(0);
const reactDice = useRef<ReactDiceRef>(null);
const [players, setPlayers] = usePlayers();
const [players] = usePlayers();
const gameInfo = useGameInfoValue();
const moveToken = useMoveToken();

useEffect(() => {
if (gameInfo.dice[0] === 0 || gameInfo.dice[1] === 0) return;
rollDice(gameInfo.dice[0], gameInfo.dice[1]);
finishMove();
}, [gameInfo.dice, finishMove]);

const moveToNextCell = (
x: number,
y: number,
tokenCoordinates: { x: number; y: number },
tokenRef: MutableRefObject<HTMLDivElement | null> | null
) => {
if (!tokenRef) return;
const ref = tokenRef as MutableRefObject<HTMLDivElement>;
tokenCoordinates.x += x;
tokenCoordinates.y += y;
ref.current.style.transform = `translate(${tokenCoordinates.x}rem, ${tokenCoordinates.y}rem)`;
};

const moveToken = async (
diceCount: number,
playerGameBoardData: GameBoardType
) => {
const tokenCoordinates = {
x: playerGameBoardData.coordinates.x,
y: playerGameBoardData.coordinates.y,
};
let tokenDirection = playerGameBoardData.direction;
let tokenLocation = playerGameBoardData.location;

for (let i = diceCount; i > 0; i--) {
const directionData = directions[tokenDirection];
moveToNextCell(
directionData.x,
directionData.y,
tokenCoordinates,
playerGameBoardData.ref
);

tokenLocation = (tokenLocation + 1) % 24;
const isCorner = CORNER_CELLS.includes(tokenLocation);

if (isCorner) {
tokenDirection = changeDirection(tokenDirection);
}

await delay(TOKEN_TRANSITION_DELAY);
}

setPlayers((prev) => {
const targetPlayerIndex = prev.findIndex(
(player) => player.playerId === gameInfo.currentPlayerId
);
const hasEscaped = tokenLocation === 6 ? false : true;

return prev.map((player, index) => {
if (index !== targetPlayerIndex) return player;
return {
...player,
gameboard: {
...player.gameboard,
location: tokenLocation,
coordinates: tokenCoordinates,
direction: tokenDirection,
hasEscaped,
},
};
});
});
};
}, [gameInfo.dice]);

const rollDice = (dice1: number, dice2: number) => {
reactDice.current?.rollAll([dice1, dice2]);
Expand Down
Loading

0 comments on commit 94752a8

Please sign in to comment.