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
3 changes: 0 additions & 3 deletions client/src/components/canvas/CanvasUI.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,6 @@ interface CanvasProps extends HTMLAttributes<HTMLDivElement> {
inkRemaining: number;
maxPixels: number;
canvasEvents: CanvasEventHandlers;
isHidden: boolean;
showInkRemaining: boolean;
}

Expand All @@ -119,7 +118,6 @@ const Canvas = forwardRef<HTMLDivElement, CanvasProps>(
inkRemaining,
maxPixels,
canvasEvents,
isHidden,
showInkRemaining,
...props
},
Expand All @@ -131,7 +129,6 @@ const Canvas = forwardRef<HTMLDivElement, CanvasProps>(
className={cn(
'relative flex w-full max-w-screen-sm flex-col border-violet-500 bg-white',
'sm:rounded-lg sm:border-4 sm:shadow-xl',
isHidden && 'hidden',
className,
)}
{...props}
Expand Down
21 changes: 9 additions & 12 deletions client/src/components/canvas/GameCanvas.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { MouseEvent as ReactMouseEvent, TouchEvent as ReactTouchEvent, useCallback, useEffect, useRef } from 'react';
import { PlayerRole, RoomStatus } from '@troublepainter/core';
import { PointerEvent, useCallback, useEffect, useRef } from 'react';
import { PlayerRole } from '@troublepainter/core';
import { Canvas } from '@/components/canvas/CanvasUI';
import { COLORS_INFO, DEFAULT_MAX_PIXELS, MAINCANVAS_RESOLUTION_WIDTH } from '@/constants/canvasConstants';
import { handleInCanvas, handleOutCanvas } from '@/handlers/canvas/cursorInOutHandler';
Expand All @@ -13,12 +13,10 @@ import { getCanvasContext } from '@/utils/getCanvasContext';
import { getDrawPoint } from '@/utils/getDrawPoint';

interface GameCanvasProps {
isHost: boolean;
role: PlayerRole;
maxPixels?: number;
currentRound: number;
roomStatus: RoomStatus;
isHidden: boolean;
isDrawable: boolean;
}

/**
Expand Down Expand Up @@ -50,7 +48,7 @@ interface GameCanvasProps {
*
* @category Components
*/
const GameCanvas = ({ role, maxPixels = DEFAULT_MAX_PIXELS, currentRound, roomStatus, isHidden }: GameCanvasProps) => {
const GameCanvas = ({ maxPixels = DEFAULT_MAX_PIXELS, currentRound, isDrawable }: GameCanvasProps) => {
const canvasRef = useRef<HTMLCanvasElement>(null);
const cursorCanvasRef = useRef<HTMLCanvasElement>(null);
const { convertCoordinate } = useCoordinateScale(MAINCANVAS_RESOLUTION_WIDTH, canvasRef);
Expand All @@ -73,7 +71,7 @@ const GameCanvas = ({ role, maxPixels = DEFAULT_MAX_PIXELS, currentRound, roomSt
redo,
makeCRDTSyncMessage,
resetCanvas,
} = useDrawing(canvasRef, roomStatus, {
} = useDrawing(canvasRef, isDrawable, {
maxPixels,
});

Expand Down Expand Up @@ -120,7 +118,7 @@ const GameCanvas = ({ role, maxPixels = DEFAULT_MAX_PIXELS, currentRound, roomSt
}));

const handleDrawStart = useCallback(
(e: ReactMouseEvent<HTMLCanvasElement> | ReactTouchEvent<HTMLCanvasElement>) => {
(e: PointerEvent<HTMLCanvasElement>) => {
if (!isConnected) return;

const { canvas } = getCanvasContext(canvasRef);
Expand All @@ -136,7 +134,7 @@ const GameCanvas = ({ role, maxPixels = DEFAULT_MAX_PIXELS, currentRound, roomSt
);

const handleDrawMove = useCallback(
(e: ReactMouseEvent<HTMLCanvasElement> | ReactTouchEvent<HTMLCanvasElement>) => {
(e: PointerEvent<HTMLCanvasElement>) => {
const { canvas } = getCanvasContext(canvasRef);
const point = getDrawPoint(e, canvas);
const convertPoint = convertCoordinate(point);
Expand All @@ -152,7 +150,7 @@ const GameCanvas = ({ role, maxPixels = DEFAULT_MAX_PIXELS, currentRound, roomSt
);

const handleDrawLeave = useCallback(
(e: ReactMouseEvent<HTMLCanvasElement> | ReactTouchEvent<HTMLCanvasElement>) => {
(e: PointerEvent<HTMLCanvasElement>) => {
const { canvas } = getCanvasContext(canvasRef);
const point = getDrawPoint(e, canvas);
const convertPoint = convertCoordinate(point);
Expand Down Expand Up @@ -202,8 +200,7 @@ const GameCanvas = ({ role, maxPixels = DEFAULT_MAX_PIXELS, currentRound, roomSt
<Canvas
canvasRef={canvasRef}
cursorCanvasRef={cursorCanvasRef}
isDrawable={(role === 'PAINTER' || role === 'DEVIL') && roomStatus === 'DRAWING'}
isHidden={isHidden}
isDrawable={isDrawable}
colors={colorsWithSelect}
brushSize={brushSize}
setBrushSize={setBrushSize}
Expand Down
56 changes: 7 additions & 49 deletions client/src/components/chat/ChatInput.tsx
Original file line number Diff line number Diff line change
@@ -1,54 +1,12 @@
import { FormEvent, memo, useMemo, useRef, useState } from 'react';
import { PlayerRole, RoomStatus, type ChatResponse } from '@troublepainter/core';
import { memo, useRef } from 'react';
import { Input } from '@/components/ui/Input';
import { chatSocketHandlers } from '@/handlers/socket/chatSocket.handler';
import { gameSocketHandlers } from '@/handlers/socket/gameSocket.handler';
import { useChat } from '@/hooks/game/useChat';
import { useShortcuts } from '@/hooks/useShortcuts';
import { useChatSocketStore } from '@/stores/socket/chatSocket.store';
import { useGameSocketStore } from '@/stores/socket/gameSocket.store';
import { useSocketStore } from '@/stores/socket/socket.store';

export const ChatInput = memo(() => {
const [inputMessage, setInputMessage] = useState('');
const inputRef = useRef<HTMLInputElement | null>(null);

// ๊ฐœ๋ณ„ Selector
const isConnected = useSocketStore((state) => state.connected.chat);
const currentPlayerId = useGameSocketStore((state) => state.currentPlayerId);
const players = useGameSocketStore((state) => state.players);
const roomStatus = useGameSocketStore((state) => state.room?.status);
const roundAssignedRole = useGameSocketStore((state) => state.roundAssignedRole);
// ์ฑ— ์•ก์…˜
const chatActions = useChatSocketStore((state) => state.actions);

const shouldDisableInput = useMemo(() => {
const ispainters = roundAssignedRole !== PlayerRole.GUESSER;
const isDrawing = roomStatus === 'DRAWING' || roomStatus === 'GUESSING';
return ispainters && isDrawing;
}, [roundAssignedRole, roomStatus]);

const handleSubmit = (e: FormEvent) => {
e.preventDefault();
if (!isConnected || !inputMessage.trim()) return;
void chatSocketHandlers.sendMessage(inputMessage);
const { submitMessage, checkDisableInput, changeMessage, inputMessage } = useChat();

const currentPlayer = players?.find((player) => player.playerId === currentPlayerId);
if (!currentPlayer || !currentPlayerId) throw new Error('Current player not found');

const messageData: ChatResponse = {
playerId: currentPlayerId as string,
nickname: currentPlayer.nickname,
message: inputMessage.trim(),
createdAt: new Date().toISOString(),
};
chatActions.addMessage(messageData);

if (roomStatus === RoomStatus.GUESSING) {
void gameSocketHandlers.checkAnswer({ answer: inputMessage });
}

setInputMessage('');
};
const inputRef = useRef<HTMLInputElement | null>(null);

useShortcuts([
{
Expand All @@ -68,13 +26,13 @@ export const ChatInput = memo(() => {
]);

return (
<form onSubmit={handleSubmit} className="mt-1 w-full">
<form onSubmit={submitMessage} className="mt-1 w-full">
<Input
ref={inputRef}
value={inputMessage}
onChange={(e) => setInputMessage(e.target.value)}
onChange={changeMessage}
placeholder="๋ฉ”์‹œ์ง€๋ฅผ ์ž…๋ ฅํ•˜์„ธ์š”"
disabled={!isConnected || shouldDisableInput}
disabled={checkDisableInput()}
autoComplete="off"
/>
</form>
Expand Down
4 changes: 2 additions & 2 deletions client/src/components/chat/ChatList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { useGameSocketStore } from '@/stores/socket/gameSocket.store';

export const ChatList = memo(() => {
const messages = useChatSocketStore((state) => state.messages);
const currentPlayerId = useGameSocketStore((state) => state.currentPlayerId);
const playerId = useGameSocketStore((state) => state.currentPlayerId);
const { containerRef } = useScrollToBottom([messages]);

return (
Expand All @@ -17,7 +17,7 @@ export const ChatList = memo(() => {
</p>

{messages.map((message) => {
const isOthers = message.playerId !== currentPlayerId;
const isOthers = message.playerId !== playerId;
return (
<ChatBubble
key={`${message.playerId}-${message.createdAt}`}
Expand Down
24 changes: 17 additions & 7 deletions client/src/components/lobby/StartButton.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,31 @@
import { Button } from '@/components/ui/Button';
import { useGameStart } from '@/hooks/useStartButton';
import { useGameStart } from '@/hooks/game/useGameStart';
import { useShortcuts } from '@/hooks/useShortcuts';
import { cn } from '@/utils/cn';

export const StartButton = () => {
const { isHost, buttonConfig, handleStartGame, isStarting } = useGameStart();
const { startGame, checkCanStart, getStartButtonStatus, isStarting } = useGameStart();
const { disabled, title, content } = getStartButtonStatus();

useShortcuts([
{
key: 'GAME_START',
action: () => startGame(),
},
]);

return (
<Button
onClick={handleStartGame}
disabled={buttonConfig.disabled || isStarting}
title={buttonConfig.title}
onClick={startGame}
disabled={disabled || isStarting}
title={title}
className={cn(
'h-full rounded-none border-0 text-xl',
'sm:rounded-2xl sm:border-2 lg:text-2xl',
!isHost && 'cursor-not-allowed opacity-50 hover:bg-violet-500',
!checkCanStart() && 'cursor-not-allowed opacity-50 hover:bg-violet-500',
)}
>
{isStarting ? '๊ณง ๊ฒŒ์ž„์ด ์‹œ์ž‘๋ฉ๋‹ˆ๋‹ค!' : buttonConfig.content}
{isStarting ? '๊ณง ๊ฒŒ์ž„์ด ์‹œ์ž‘๋ฉ๋‹ˆ๋‹ค!' : content}
</Button>
);
};
10 changes: 2 additions & 8 deletions client/src/components/modal/RoleModal.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,11 @@
import { useEffect } from 'react';
import { Modal } from '@/components/ui/Modal';
import { PLAYING_ROLE_TEXT } from '@/constants/gameConstant';
import { useModal } from '@/hooks/useModal';
import { useRoleModal } from '@/hooks/game/useRoleModal';
import { useGameSocketStore } from '@/stores/socket/gameSocket.store';

const RoleModal = () => {
const room = useGameSocketStore((state) => state.room);
const { isModalOpened, closeModal, handleKeyDown } = useRoleModal();
const roundAssignedRole = useGameSocketStore((state) => state.roundAssignedRole);
const { isModalOpened, closeModal, handleKeyDown, openModal } = useModal(5000);

useEffect(() => {
if (roundAssignedRole) openModal();
}, [roundAssignedRole, room?.currentRound]);

return (
<Modal
Expand Down
81 changes: 7 additions & 74 deletions client/src/components/modal/RoundEndModal.tsx
Original file line number Diff line number Diff line change
@@ -1,81 +1,21 @@
import { useEffect, useState } from 'react';
import { DotLottieReact } from '@lottiefiles/dotlottie-react';
import { PlayerRole, RoomStatus } from '@troublepainter/core';
import roundLoss from '@/assets/lottie/round-loss.lottie';
import roundWin from '@/assets/lottie/round-win.lottie';
import gameLoss from '@/assets/sounds/game-loss.mp3';
import gameWin from '@/assets/sounds/game-win.mp3';
import { Modal } from '@/components/ui/Modal';
import { useModal } from '@/hooks/useModal';
import { useRoundEndModal } from '@/hooks/game/useRoundEndModal';
import { useGameSocketStore } from '@/stores/socket/gameSocket.store';
import { useTimerStore } from '@/stores/timer.store';
import { cn } from '@/utils/cn';
import { SOUND_IDS, SoundManager } from '@/utils/soundManager';

const RoundEndModal = () => {
const room = useGameSocketStore((state) => state.room);
const roundWinners = useGameSocketStore((state) => state.roundWinners);
const players = useGameSocketStore((state) => state.players);
const currentPlayerId = useGameSocketStore((state) => state.currentPlayerId);
const timer = useTimerStore((state) => state.timers.ENDING);

const { isModalOpened, openModal, closeModal } = useModal();
const [showAnimation, setShowAnimation] = useState(false);
const [isAnimationFading, setIsAnimationFading] = useState(false);

const devil = players.find((player) => player.role === PlayerRole.DEVIL);
const isDevilWin = roundWinners?.some((winner) => winner.role === PlayerRole.DEVIL);
const isCurrentPlayerWinner = roundWinners?.some((winner) => winner.playerId === currentPlayerId);

// ์ปดํฌ๋„ŒํŠธ ๋งˆ์šดํŠธ ์‹œ ์‚ฌ์šด๋“œ ๋ฏธ๋ฆฌ ๋กœ๋“œ
const soundManager = SoundManager.getInstance();
useEffect(() => {
soundManager.preloadSound(SOUND_IDS.WIN, gameWin);
soundManager.preloadSound(SOUND_IDS.LOSS, gameLoss);
}, [soundManager]);

useEffect(() => {
if (roundWinners) {
setIsAnimationFading(false);
setShowAnimation(true);
openModal();

if (isCurrentPlayerWinner) {
void soundManager.playSound(SOUND_IDS.WIN, 0.3);
} else {
void soundManager.playSound(SOUND_IDS.LOSS, 0.3);
}
}
}, [roundWinners]);

useEffect(() => {
if (room && room.status === RoomStatus.DRAWING) closeModal();
}, [room]);

useEffect(() => {
if (showAnimation) {
// 3์ดˆ ํ›„์— ํŽ˜์ด๋“œ์•„์›ƒ ์‹œ์ž‘
const fadeTimer = setTimeout(() => {
setIsAnimationFading(true);
}, 3000);

// 3.5์ดˆ ํ›„์— ์ปดํฌ๋„ŒํŠธ ์ œ๊ฑฐ
const removeTimer = setTimeout(() => {
setShowAnimation(false);
}, 3500);

return () => {
clearTimeout(fadeTimer);
clearTimeout(removeTimer);
};
}
}, [showAnimation]);
const { showAnimation, isPlayerWinner, isAnimationFading, isModalOpened, timer, isDevilWin, solver, devil } =
useRoundEndModal();
const currentWord = useGameSocketStore((state) => state.room?.currentWord);

return (
<>
{/* ์Šน๋ฆฌ/ํŒจ๋ฐฐ ์• ๋‹ˆ๋ฉ”์ด์…˜ */}
{showAnimation &&
(isCurrentPlayerWinner ? (
(isPlayerWinner ? (
<DotLottieReact
src={roundWin}
autoplay
Expand All @@ -97,11 +37,7 @@ const RoundEndModal = () => {
/>
))}

<Modal
title={room?.currentWord || ''}
isModalOpened={isModalOpened}
className="max-w-[26.875rem] sm:max-w-[61.75rem]"
>
<Modal title={currentWord || ''} isModalOpened={isModalOpened} className="max-w-[26.875rem] sm:max-w-[61.75rem]">
<div className="relative flex min-h-[12rem] items-center justify-center sm:min-h-[15.75rem]">
<span className="absolute right-2 top-2 flex h-8 w-8 items-center justify-center rounded-full border-2 border-violet-300 text-base text-violet-300">
{timer}
Expand All @@ -111,10 +47,7 @@ const RoundEndModal = () => {
<> ์ •๋‹ต์„ ๋งž์ถ˜ ๊ตฌ๊ฒฝ๊พผ์ด ์—†์Šต๋‹ˆ๋‹ค</>
) : (
<>
๊ตฌ๊ฒฝ๊พผ{' '}
<span className="text-violet-600">
{roundWinners?.find((winner) => winner.role === PlayerRole.GUESSER)?.nickname}
</span>
๊ตฌ๊ฒฝ๊พผ <span className="text-violet-600">{solver?.nickname}</span>
์ด(๊ฐ€) ์ •๋‹ต์„ ๋งžํ˜”์Šต๋‹ˆ๋‹ค
</>
)}
Expand Down
Loading