Builds for
1 pipeline
passed
in
7 minutes 51 seconds
Merge branch 'feature/ingame' into develop
Showing
14 changed files
with
422 additions
and
14 deletions
| ... | @@ -25,6 +25,7 @@ | ... | @@ -25,6 +25,7 @@ |
| 25 | "scripts": { | 25 | "scripts": { |
| 26 | "start": "npm run twcss && set PORT=3001 && react-scripts start", | 26 | "start": "npm run twcss && set PORT=3001 && react-scripts start", |
| 27 | "build": "npm run twcss && react-scripts build", | 27 | "build": "npm run twcss && react-scripts build", |
| 28 | + "onlybuild": "react-scripts build", | ||
| 28 | "test": "react-scripts test", | 29 | "test": "react-scripts test", |
| 29 | "eject": "react-scripts eject", | 30 | "eject": "react-scripts eject", |
| 30 | "twcss": "tailwind build src/tailwind.css -c tailwind.config.js -o src/index.css" | 31 | "twcss": "tailwind build src/tailwind.css -c tailwind.config.js -o src/index.css" | ... | ... |
| ... | @@ -18,5 +18,15 @@ export const MessageType = { | ... | @@ -18,5 +18,15 @@ export const MessageType = { |
| 18 | ROOM_USER_UPDATE: "updateRoomUser", | 18 | ROOM_USER_UPDATE: "updateRoomUser", |
| 19 | ROOM_CHAT: "chat", | 19 | ROOM_CHAT: "chat", |
| 20 | ROOM_READY: "ready", | 20 | ROOM_READY: "ready", |
| 21 | - ROOM_START: "startGame" | 21 | + ROOM_START: "startGame", |
| 22 | + GAME_START: "startRound", | ||
| 23 | + GAME_WORDSET: "wordSet", | ||
| 24 | + GAME_CHOOSE: "chooseWord", | ||
| 25 | + GAME_WORD: "wordChosen", | ||
| 26 | + GAME_TIMER: "timer", | ||
| 27 | + GAME_ACCEPT: "answerAccepted", | ||
| 28 | + GAME_FINISH_ROUND: "finishRound", | ||
| 29 | + GAME_FINISH_GAME: "finishGame", | ||
| 30 | + DRAW_SET: "setBrush", | ||
| 31 | + DRAW_MOVE: "moveBrush", | ||
| 22 | } as const | 32 | } as const |
| ... | \ No newline at end of file | ... | \ No newline at end of file | ... | ... |
| 1 | -import React, { useCallback, useEffect, useRef, useState } from 'react'; | 1 | +import React, { useCallback, useContext, useEffect, useRef, useState } from 'react'; |
| 2 | -import { Vector } from './types'; | 2 | +import SocketContext from '../../contexts/SocketContext'; |
| 3 | +import { MessageType, RawMessage } from '../common/types'; | ||
| 4 | +import { BrushData, Vector } from './types'; | ||
| 3 | 5 | ||
| 4 | // 참고 : https://basketdeveloper.tistory.com/79 | 6 | // 참고 : https://basketdeveloper.tistory.com/79 |
| 5 | 7 | ||
| 6 | -export const Canvas: React.FC = () => { | 8 | +interface CanvasProps { |
| 9 | + isDrawer: boolean; | ||
| 10 | +} | ||
| 11 | + | ||
| 12 | +export const Canvas: React.FC<CanvasProps> = ({ isDrawer }) => { | ||
| 13 | + const socket = useContext(SocketContext); | ||
| 7 | const canvasRef = useRef<HTMLCanvasElement>(null); | 14 | const canvasRef = useRef<HTMLCanvasElement>(null); |
| 8 | 15 | ||
| 9 | const [mousePosition, setMousePosition] = useState<Vector>({ x:0, y:0 }); | 16 | const [mousePosition, setMousePosition] = useState<Vector>({ x:0, y:0 }); |
| ... | @@ -52,6 +59,22 @@ export const Canvas: React.FC = () => { | ... | @@ -52,6 +59,22 @@ export const Canvas: React.FC = () => { |
| 52 | if (coordinates) { | 59 | if (coordinates) { |
| 53 | setIsPainting(true); | 60 | setIsPainting(true); |
| 54 | setMousePosition(coordinates); | 61 | setMousePosition(coordinates); |
| 62 | + | ||
| 63 | + const rawMessage: RawMessage = { | ||
| 64 | + type: MessageType.DRAW_MOVE, | ||
| 65 | + message: coordinates | ||
| 66 | + }; | ||
| 67 | + socket.emit('msg', rawMessage, () => {}); | ||
| 68 | + | ||
| 69 | + const nextRawMessage: RawMessage = { | ||
| 70 | + type: MessageType.DRAW_SET, | ||
| 71 | + message: { | ||
| 72 | + size: 5, | ||
| 73 | + color: '000000', | ||
| 74 | + drawing: true | ||
| 75 | + } as BrushData | ||
| 76 | + }; | ||
| 77 | + socket.emit('msg', nextRawMessage, () => {}); | ||
| 55 | } | 78 | } |
| 56 | }, []); | 79 | }, []); |
| 57 | 80 | ||
| ... | @@ -65,6 +88,13 @@ export const Canvas: React.FC = () => { | ... | @@ -65,6 +88,13 @@ export const Canvas: React.FC = () => { |
| 65 | const newMousePosition = getCoordinates(event); | 88 | const newMousePosition = getCoordinates(event); |
| 66 | if (mousePosition && newMousePosition) { | 89 | if (mousePosition && newMousePosition) { |
| 67 | drawLine(mousePosition, newMousePosition); | 90 | drawLine(mousePosition, newMousePosition); |
| 91 | + | ||
| 92 | + const rawMessage: RawMessage = { | ||
| 93 | + type: MessageType.DRAW_MOVE, | ||
| 94 | + message: newMousePosition | ||
| 95 | + }; | ||
| 96 | + socket.emit('msg', rawMessage, () => {}); | ||
| 97 | + | ||
| 68 | setMousePosition(newMousePosition); | 98 | setMousePosition(newMousePosition); |
| 69 | } | 99 | } |
| 70 | } | 100 | } |
| ... | @@ -73,11 +103,39 @@ export const Canvas: React.FC = () => { | ... | @@ -73,11 +103,39 @@ export const Canvas: React.FC = () => { |
| 73 | ); | 103 | ); |
| 74 | 104 | ||
| 75 | const exitPaint = useCallback(() => { | 105 | const exitPaint = useCallback(() => { |
| 106 | + const rawMessage: RawMessage = { | ||
| 107 | + type: MessageType.DRAW_SET, | ||
| 108 | + message: { | ||
| 109 | + size: 5, | ||
| 110 | + color: '000000', | ||
| 111 | + drawing: false | ||
| 112 | + } as BrushData | ||
| 113 | + }; | ||
| 114 | + socket.emit('msg', rawMessage, () => {}); | ||
| 115 | + | ||
| 76 | setIsPainting(false); | 116 | setIsPainting(false); |
| 77 | }, []); | 117 | }, []); |
| 78 | 118 | ||
| 119 | + const handleDrawSet = useCallback((rawMessage: RawMessage) => { | ||
| 120 | + if (rawMessage.type === MessageType.DRAW_SET) { | ||
| 121 | + const data = rawMessage.message as BrushData; | ||
| 122 | + setIsPainting(data.drawing); | ||
| 123 | + } | ||
| 124 | + }, []); | ||
| 125 | + | ||
| 126 | + const handleDrawMove = useCallback((rawMessage: RawMessage) => { | ||
| 127 | + if (rawMessage.type === MessageType.DRAW_MOVE) { | ||
| 128 | + const data = rawMessage.message as Vector; | ||
| 129 | + if (isPainting) { | ||
| 130 | + drawLine(mousePosition, data); | ||
| 131 | + } | ||
| 132 | + setMousePosition(data); | ||
| 133 | + } | ||
| 134 | + }, [isPainting, mousePosition]) | ||
| 135 | + | ||
| 79 | useEffect(() => { | 136 | useEffect(() => { |
| 80 | if (canvasRef.current) { | 137 | if (canvasRef.current) { |
| 138 | + if (isDrawer) { | ||
| 81 | const canvas: HTMLCanvasElement = canvasRef.current; | 139 | const canvas: HTMLCanvasElement = canvasRef.current; |
| 82 | 140 | ||
| 83 | canvas.addEventListener('mousedown', startPaint); | 141 | canvas.addEventListener('mousedown', startPaint); |
| ... | @@ -92,11 +150,39 @@ export const Canvas: React.FC = () => { | ... | @@ -92,11 +150,39 @@ export const Canvas: React.FC = () => { |
| 92 | canvas.removeEventListener('mouseleave', exitPaint); | 150 | canvas.removeEventListener('mouseleave', exitPaint); |
| 93 | }; | 151 | }; |
| 94 | } | 152 | } |
| 95 | - }, [startPaint, paint, exitPaint]); | 153 | + } |
| 154 | + }, [isDrawer, startPaint, paint, exitPaint]); | ||
| 155 | + | ||
| 156 | + useEffect(() => { | ||
| 157 | + if (!isDrawer) { | ||
| 158 | + socket.on('msg', handleDrawSet); | ||
| 159 | + socket.on('msg', handleDrawMove); | ||
| 160 | + | ||
| 161 | + return () => { | ||
| 162 | + socket.off('msg', handleDrawSet); | ||
| 163 | + socket.off('msg', handleDrawMove); | ||
| 164 | + } | ||
| 165 | + } | ||
| 166 | + }, [isDrawer, handleDrawMove]); | ||
| 167 | + | ||
| 168 | + const handleClearWhenStart = useCallback((rawMessage: RawMessage) => { | ||
| 169 | + if (rawMessage.type === MessageType.GAME_START) { | ||
| 170 | + clearCanvas(); | ||
| 171 | + setIsPainting(false); | ||
| 172 | + // TODO: 펜 굵기, 색 설정하게 되면 여기에 초기화 넣기 | ||
| 173 | + } | ||
| 174 | + }, []); | ||
| 175 | + | ||
| 176 | + useEffect(() => { | ||
| 177 | + socket.on('msg', handleClearWhenStart); | ||
| 178 | + return () => { | ||
| 179 | + socket.off('msg', handleClearWhenStart); | ||
| 180 | + } | ||
| 181 | + }, []); | ||
| 96 | 182 | ||
| 97 | return ( | 183 | return ( |
| 98 | <div className='mx-3 px-2 py-1 rounded shadow'> | 184 | <div className='mx-3 px-2 py-1 rounded shadow'> |
| 99 | - <canvas ref={canvasRef} width='512' height='384' /> | 185 | + <canvas ref={canvasRef} width='640' height='480' /> |
| 100 | </div> | 186 | </div> |
| 101 | ); | 187 | ); |
| 102 | } | 188 | } |
| ... | \ No newline at end of file | ... | \ No newline at end of file | ... | ... |
| ... | @@ -23,9 +23,13 @@ export const Chat: React.FC<ChatProps> = (props) => { | ... | @@ -23,9 +23,13 @@ export const Chat: React.FC<ChatProps> = (props) => { |
| 23 | } | 23 | } |
| 24 | 24 | ||
| 25 | socket.on('msg', handleChatData); | 25 | socket.on('msg', handleChatData); |
| 26 | + socket.on('msg', handleAcceptMessage); | ||
| 27 | + socket.on('msg', handleFinishMessage); | ||
| 26 | 28 | ||
| 27 | return () => { | 29 | return () => { |
| 28 | socket.off('msg', handleChatData); | 30 | socket.off('msg', handleChatData); |
| 31 | + socket.off('msg', handleAcceptMessage); | ||
| 32 | + socket.on('msg', handleFinishMessage); | ||
| 29 | } | 33 | } |
| 30 | }, []); | 34 | }, []); |
| 31 | 35 | ||
| ... | @@ -47,9 +51,29 @@ export const Chat: React.FC<ChatProps> = (props) => { | ... | @@ -47,9 +51,29 @@ export const Chat: React.FC<ChatProps> = (props) => { |
| 47 | } | 51 | } |
| 48 | }, [input]); | 52 | }, [input]); |
| 49 | 53 | ||
| 54 | + const handleAcceptMessage = useCallback((rawMessage: RawMessage) => { | ||
| 55 | + if (rawMessage.type === MessageType.GAME_ACCEPT) { | ||
| 56 | + const message: ChatData = { | ||
| 57 | + sender: 'SYSTEM', | ||
| 58 | + message: 'That\'s correct!' | ||
| 59 | + }; | ||
| 60 | + setChatLines(oldChatLines => [...oldChatLines, message]); | ||
| 61 | + } | ||
| 62 | + }, []); | ||
| 63 | + | ||
| 64 | + const handleFinishMessage = useCallback((rawMessage: RawMessage) => { | ||
| 65 | + if (rawMessage.type === MessageType.GAME_FINISH_ROUND) { | ||
| 66 | + const message: ChatData = { | ||
| 67 | + sender: 'SYSTEM', | ||
| 68 | + message: 'The round is over!' | ||
| 69 | + }; | ||
| 70 | + setChatLines(oldChatLines => [...oldChatLines, message]); | ||
| 71 | + } | ||
| 72 | + }, []); | ||
| 73 | + | ||
| 50 | return ( | 74 | return ( |
| 51 | <div className={props.w}> | 75 | <div className={props.w}> |
| 52 | - <div className={`${props.h} w-full rounded shadow flex flex-col overflow-y-scroll`}> | 76 | + <div className={`${props.h} w-full py-2 rounded shadow flex flex-col overflow-y-scroll`}> |
| 53 | {chatLines.map((line, i) => (<ChatLine key={16383+i} chatData={line}/>))} | 77 | {chatLines.map((line, i) => (<ChatLine key={16383+i} chatData={line}/>))} |
| 54 | <div ref={messageEndRef} /> | 78 | <div ref={messageEndRef} /> |
| 55 | </div> | 79 | </div> | ... | ... |
web/src/components/room/GameBoard.tsx
0 → 100644
| 1 | +import React, { useCallback, useContext, useEffect, useState } from 'react'; | ||
| 2 | +import { useLocation } from 'react-router'; | ||
| 3 | +import SocketContext from '../../contexts/SocketContext'; | ||
| 4 | +import { MessageType, RawMessage } from '../common/types'; | ||
| 5 | +import { Canvas } from './Canvas'; | ||
| 6 | +import { RoundInfo } from './RoundInfo'; | ||
| 7 | +import { Role, RoundData } from './types'; | ||
| 8 | +import { Word } from './Word'; | ||
| 9 | + | ||
| 10 | +interface GameBoardLocation { | ||
| 11 | + state: { username: string } | ||
| 12 | +} | ||
| 13 | + | ||
| 14 | +interface GameBoardProps { | ||
| 15 | + isInGame: boolean | ||
| 16 | +} | ||
| 17 | + | ||
| 18 | +export const GameBoard: React.FC<GameBoardProps> = ({ isInGame }) => { | ||
| 19 | + const socket = useContext(SocketContext); | ||
| 20 | + const location: GameBoardLocation = useLocation(); | ||
| 21 | + | ||
| 22 | + const [ isDrawer, setIsDrawer ] = useState(false); | ||
| 23 | + const [ words, setWords ] = useState<string[]>([]); | ||
| 24 | + const [ wordChosen, setWordChosen ] = useState(''); | ||
| 25 | + const [ round, setRound ] = useState(0); | ||
| 26 | + | ||
| 27 | + const handleWordSet = useCallback((rawMessage: RawMessage) => { | ||
| 28 | + if (rawMessage.type === MessageType.GAME_WORDSET) { | ||
| 29 | + console.log('단어 도착'); | ||
| 30 | + const { words } = rawMessage.message as { words: string[] }; | ||
| 31 | + setWords(words); | ||
| 32 | + } | ||
| 33 | + }, []); | ||
| 34 | + | ||
| 35 | + const handleStart = useCallback((rawMessage: RawMessage) => { | ||
| 36 | + if (rawMessage.type === MessageType.GAME_START) { | ||
| 37 | + setWords([]); | ||
| 38 | + | ||
| 39 | + const data = rawMessage.message as RoundData; | ||
| 40 | + console.log('테스트 location ', location.state.username); | ||
| 41 | + console.log('테스트 rolse ', data.roles); | ||
| 42 | + const index = data.roles.findIndex(x => x.username === location.state.username); | ||
| 43 | + setIsDrawer(data.roles[index].role === 'drawer'); | ||
| 44 | + setWordChosen(''); | ||
| 45 | + setRound(data.round); | ||
| 46 | + } | ||
| 47 | + }, []); | ||
| 48 | + | ||
| 49 | + const handleGetWordLength = useCallback((rawMessage: RawMessage) => { | ||
| 50 | + if (rawMessage.type === MessageType.GAME_WORD) { | ||
| 51 | + if (wordChosen === '') { | ||
| 52 | + const { length } = rawMessage.message as { length: number }; | ||
| 53 | + setWordChosen('_' + ' _'.repeat(length-1)); | ||
| 54 | + } | ||
| 55 | + } | ||
| 56 | + }, [wordChosen]); | ||
| 57 | + | ||
| 58 | + const handleAnswer = useCallback((rawMessage: RawMessage) => { | ||
| 59 | + if (rawMessage.type === MessageType.GAME_ACCEPT || rawMessage.type === MessageType.GAME_FINISH_ROUND) { | ||
| 60 | + const { answer } = rawMessage.message as { answer: string }; | ||
| 61 | + setWordChosen(answer); | ||
| 62 | + } | ||
| 63 | + }, []); | ||
| 64 | + | ||
| 65 | + useEffect(() => { | ||
| 66 | + socket.on('msg', handleStart); | ||
| 67 | + socket.on('msg', handleGetWordLength); | ||
| 68 | + socket.on('msg', handleWordSet); | ||
| 69 | + socket.on('msg', handleAnswer); | ||
| 70 | + | ||
| 71 | + return () => { | ||
| 72 | + socket.off('msg', handleStart); | ||
| 73 | + socket.off('msg', handleGetWordLength); | ||
| 74 | + socket.off('msg', handleWordSet); | ||
| 75 | + socket.off('msg', handleAnswer); | ||
| 76 | + } | ||
| 77 | + }, []); | ||
| 78 | + | ||
| 79 | + return ( | ||
| 80 | + <div className={`w-auto ${isInGame ? '' : 'hidden'}`}> | ||
| 81 | + <div className='w-full flex flex-col justify-center items-center'> | ||
| 82 | + {words.map((word, i) => (<Word key={word} index={i} word={word} setWordChosen={setWordChosen} setWords={setWords} />))} | ||
| 83 | + </div> | ||
| 84 | + <Canvas isDrawer={isDrawer && wordChosen !== ''} /> | ||
| 85 | + <RoundInfo round={round} wordChosen={wordChosen} /> | ||
| 86 | + </div> | ||
| 87 | + ); | ||
| 88 | +} | ||
| ... | \ No newline at end of file | ... | \ No newline at end of file |
| ... | @@ -33,7 +33,7 @@ export const Ready: React.FC<ReadyProps> = ({ users }) => { | ... | @@ -33,7 +33,7 @@ export const Ready: React.FC<ReadyProps> = ({ users }) => { |
| 33 | const handleReady = useCallback(() => { | 33 | const handleReady = useCallback(() => { |
| 34 | if (isAdmin && isAllReady) { | 34 | if (isAdmin && isAllReady) { |
| 35 | const rawMessage: RawMessage = { | 35 | const rawMessage: RawMessage = { |
| 36 | - type: MessageType.ROOM_READY, | 36 | + type: MessageType.ROOM_START, |
| 37 | message: {} | 37 | message: {} |
| 38 | } | 38 | } |
| 39 | socket.emit('msg', rawMessage, () => {}); | 39 | socket.emit('msg', rawMessage, () => {}); | ... | ... |
| ... | @@ -10,7 +10,7 @@ interface RoomInfoProps { | ... | @@ -10,7 +10,7 @@ interface RoomInfoProps { |
| 10 | 10 | ||
| 11 | export const RoomInfo: React.FC<RoomInfoProps> = ({ roomData }) => { | 11 | export const RoomInfo: React.FC<RoomInfoProps> = ({ roomData }) => { |
| 12 | return ( | 12 | return ( |
| 13 | - <div className='m-3 mb-8 w-5/6 flex items-center place-content-between'> | 13 | + <div className='m-3 my-8 w-5/6 flex items-center place-content-between'> |
| 14 | <div>{roomData.name}</div> | 14 | <div>{roomData.name}</div> |
| 15 | <div>{roomData.users.length}/{roomData.maxUsers}</div> | 15 | <div>{roomData.users.length}/{roomData.maxUsers}</div> |
| 16 | </div> | 16 | </div> | ... | ... |
web/src/components/room/RoundInfo.tsx
0 → 100644
| 1 | +import React from 'react'; | ||
| 2 | +import SocketContext from '../../contexts/SocketContext'; | ||
| 3 | +import { Timer } from './Timer'; | ||
| 4 | +import { RoundData } from './types'; | ||
| 5 | + | ||
| 6 | +interface RoundInfoProps { | ||
| 7 | + round: number; | ||
| 8 | + wordChosen: string; | ||
| 9 | +} | ||
| 10 | + | ||
| 11 | +export const RoundInfo: React.FC<RoundInfoProps> = ({ round, wordChosen }) => { | ||
| 12 | + return ( | ||
| 13 | + <div className='p-3 m-3 h-14 rounded shadow flex items-center place-content-between'> | ||
| 14 | + <Timer /> | ||
| 15 | + <div>{wordChosen}</div> | ||
| 16 | + <div>Round {round}/5</div> | ||
| 17 | + </div> | ||
| 18 | + ); | ||
| 19 | +} | ||
| ... | \ No newline at end of file | ... | \ No newline at end of file |
web/src/components/room/Timer.tsx
0 → 100644
| 1 | +import React, { useCallback, useContext, useEffect, useState } from 'react'; | ||
| 2 | +import SocketContext from '../../contexts/SocketContext'; | ||
| 3 | +import { MessageType, RawMessage } from '../common/types'; | ||
| 4 | + | ||
| 5 | +interface timer { | ||
| 6 | + state: "started" | "stopped"; | ||
| 7 | + time: number; | ||
| 8 | +}; | ||
| 9 | + | ||
| 10 | +export const Timer: React.FC = () => { | ||
| 11 | + const socket = useContext(SocketContext); | ||
| 12 | + | ||
| 13 | + const [ time, setTime ] = useState(0); | ||
| 14 | + const [ isStop, setIsStop ] = useState(true); | ||
| 15 | + | ||
| 16 | + const handleTimeSet = useCallback((rawMessage: RawMessage) => { | ||
| 17 | + if (rawMessage.type === MessageType.GAME_TIMER) { | ||
| 18 | + const data = rawMessage.message as timer; | ||
| 19 | + | ||
| 20 | + console.log(data); | ||
| 21 | + if (data.state === 'started') { | ||
| 22 | + setIsStop(false); | ||
| 23 | + } else { | ||
| 24 | + setIsStop(true); | ||
| 25 | + } | ||
| 26 | + | ||
| 27 | + setTime(Math.floor(data.time)); | ||
| 28 | + } | ||
| 29 | + }, []); | ||
| 30 | + | ||
| 31 | + useEffect(() => { | ||
| 32 | + if (!isStop) { | ||
| 33 | + const go = setInterval(() => { | ||
| 34 | + setTime(time-1); | ||
| 35 | + }, 1000); | ||
| 36 | + | ||
| 37 | + return () => clearInterval(go); | ||
| 38 | + } else { | ||
| 39 | + setTime(0); | ||
| 40 | + } | ||
| 41 | + }, [time, isStop]); | ||
| 42 | + | ||
| 43 | + useEffect(() => { | ||
| 44 | + socket.on('msg', handleTimeSet); | ||
| 45 | + return () => { | ||
| 46 | + socket.off('msg', handleTimeSet); | ||
| 47 | + } | ||
| 48 | + }, []); | ||
| 49 | + | ||
| 50 | + return ( | ||
| 51 | + <div className={time < 10 ? 'text-red-500' : 'text-black'}> | ||
| 52 | + 🕒 {time} | ||
| 53 | + </div> | ||
| 54 | + ); | ||
| 55 | +} | ||
| ... | \ No newline at end of file | ... | \ No newline at end of file |
web/src/components/room/UserRole.tsx
0 → 100644
| 1 | +import React, { useCallback, useContext, useEffect, useState } from 'react'; | ||
| 2 | +import SocketContext from '../../contexts/SocketContext'; | ||
| 3 | +import { MessageType, RawMessage } from '../common/types'; | ||
| 4 | +import { RoleData, RoundData } from './types'; | ||
| 5 | + | ||
| 6 | +interface UserRoleProps { | ||
| 7 | + isInGame: boolean; | ||
| 8 | +} | ||
| 9 | + | ||
| 10 | +export const UserRole: React.FC<UserRoleProps> = ({ isInGame }) => { | ||
| 11 | + const socket = useContext(SocketContext); | ||
| 12 | + const [ roles, setRoles ] = useState<RoleData[]>([]); | ||
| 13 | + | ||
| 14 | + const handleRole = useCallback((rawMessage: RawMessage) => { | ||
| 15 | + if (rawMessage.type === MessageType.GAME_START) { | ||
| 16 | + const { roles } = rawMessage.message as RoundData; | ||
| 17 | + setRoles(roles); | ||
| 18 | + } | ||
| 19 | + }, []); | ||
| 20 | + | ||
| 21 | + useEffect(() => { | ||
| 22 | + socket.on('msg', handleRole); | ||
| 23 | + return () => { | ||
| 24 | + socket.off('msg', handleRole); | ||
| 25 | + } | ||
| 26 | + }, []); | ||
| 27 | + | ||
| 28 | + return ( | ||
| 29 | + <div className={`w-40 h-140 rounded shadow flex flex-col items-center ${isInGame ? '' : 'hidden'}`}> | ||
| 30 | + <div className='mt-3' /> | ||
| 31 | + {roles.map(x => ( | ||
| 32 | + <div key={x.username} className={`my-5 ease-linear transition-all duration-100 | ||
| 33 | + ${x.role === 'drawer' ? 'text-blue-500' | ||
| 34 | + : x.role === 'winner' ? 'text-green-500' | ||
| 35 | + : 'text-black'}`}> | ||
| 36 | + {x.nickname} {x.role === 'drawer' ? '🖌️' : x.role === 'spectator' ? '👻' : ''} | ||
| 37 | + </div> | ||
| 38 | + ))} | ||
| 39 | + </div> | ||
| 40 | + ); | ||
| 41 | +} |
web/src/components/room/Word.tsx
0 → 100644
| 1 | +import React, { useCallback, useContext } from 'react'; | ||
| 2 | +import { IndexType } from 'typescript'; | ||
| 3 | +import SocketContext from '../../contexts/SocketContext'; | ||
| 4 | +import { MessageResponse, MessageType, RawMessage } from '../common/types'; | ||
| 5 | + | ||
| 6 | +interface WordProps { | ||
| 7 | + index: number; | ||
| 8 | + word: string; | ||
| 9 | + setWordChosen: (value: React.SetStateAction<string>) => void; | ||
| 10 | + setWords: (value: React.SetStateAction<string[]>) => void; | ||
| 11 | +} | ||
| 12 | + | ||
| 13 | +export const Word: React.FC<WordProps> = (props) => { | ||
| 14 | + const socket = useContext(SocketContext); | ||
| 15 | + | ||
| 16 | + const handleChoose = useCallback(() => { | ||
| 17 | + const rawMessage: RawMessage = { | ||
| 18 | + type: MessageType.GAME_CHOOSE, | ||
| 19 | + message: { word: props.word } | ||
| 20 | + }; | ||
| 21 | + socket.emit('msg', rawMessage, (response: MessageResponse<undefined>) => { | ||
| 22 | + if (response.ok) { | ||
| 23 | + props.setWords([]); | ||
| 24 | + props.setWordChosen(props.word); | ||
| 25 | + } | ||
| 26 | + }); | ||
| 27 | + }, [props.setWordChosen]); | ||
| 28 | + | ||
| 29 | + return ( | ||
| 30 | + <button className={`bg-green-500 active:bg-green-600 fixed | ||
| 31 | + text-white font-bold ${'mt-' + 40*(props.index+2)} mt-40 | ||
| 32 | + px-5 py-2 rounded shadow | ||
| 33 | + outline-none focus:outline-none hover:shadow-md | ||
| 34 | + ease-linear transition-all duration-100`} | ||
| 35 | + type="button" | ||
| 36 | + onClick={() => handleChoose()}>{props.word}</button> | ||
| 37 | + ); | ||
| 38 | +} |
| ... | @@ -26,3 +26,23 @@ export interface Vector { | ... | @@ -26,3 +26,23 @@ export interface Vector { |
| 26 | x: number; | 26 | x: number; |
| 27 | y: number; | 27 | y: number; |
| 28 | } | 28 | } |
| 29 | + | ||
| 30 | +export type Role = "drawer" | "guesser" | "winner" | "spectator"; | ||
| 31 | + | ||
| 32 | +export interface RoleData { | ||
| 33 | + username: string; | ||
| 34 | + nickname: string; | ||
| 35 | + role: Role; | ||
| 36 | +} | ||
| 37 | + | ||
| 38 | +export interface RoundData { | ||
| 39 | + round: number; | ||
| 40 | + duration: number; | ||
| 41 | + roles: RoleData[]; | ||
| 42 | +}; | ||
| 43 | + | ||
| 44 | +export interface BrushData { | ||
| 45 | + size: number; | ||
| 46 | + color: string; | ||
| 47 | + drawing: boolean; | ||
| 48 | +} | ... | ... |
| ... | @@ -4,10 +4,12 @@ import { Main } from '../components/common/Main'; | ... | @@ -4,10 +4,12 @@ import { Main } from '../components/common/Main'; |
| 4 | import { MessageResponse, MessageType, RawMessage } from '../components/common/types'; | 4 | import { MessageResponse, MessageType, RawMessage } from '../components/common/types'; |
| 5 | import { Canvas } from '../components/room/Canvas'; | 5 | import { Canvas } from '../components/room/Canvas'; |
| 6 | import { Chat } from '../components/room/Chat'; | 6 | import { Chat } from '../components/room/Chat'; |
| 7 | +import { GameBoard } from '../components/room/GameBoard'; | ||
| 7 | import { Ready } from '../components/room/Ready'; | 8 | import { Ready } from '../components/room/Ready'; |
| 8 | import { RoomInfo } from '../components/room/RoomInfo'; | 9 | import { RoomInfo } from '../components/room/RoomInfo'; |
| 9 | import { RoomData, UpdateRoomUser } from '../components/room/types'; | 10 | import { RoomData, UpdateRoomUser } from '../components/room/types'; |
| 10 | import { UserInfo } from '../components/room/UserInfo'; | 11 | import { UserInfo } from '../components/room/UserInfo'; |
| 12 | +import { UserRole } from '../components/room/UserRole'; | ||
| 11 | import { UserStatus } from '../components/room/UserStatus'; | 13 | import { UserStatus } from '../components/room/UserStatus'; |
| 12 | import SocketContext from '../contexts/SocketContext'; | 14 | import SocketContext from '../contexts/SocketContext'; |
| 13 | 15 | ||
| ... | @@ -29,6 +31,14 @@ export const Room: React.FC = () => { | ... | @@ -29,6 +31,14 @@ export const Room: React.FC = () => { |
| 29 | }); | 31 | }); |
| 30 | const [ isInGame, setIsInGame ] = useState(false); | 32 | const [ isInGame, setIsInGame ] = useState(false); |
| 31 | 33 | ||
| 34 | + const handleInGame = useCallback((rawMessage: RawMessage) => { | ||
| 35 | + if (rawMessage.type === MessageType.GAME_START) { | ||
| 36 | + setIsInGame(true); | ||
| 37 | + } else if (rawMessage.type === MessageType.GAME_FINISH_GAME) { | ||
| 38 | + setIsInGame(false); | ||
| 39 | + } | ||
| 40 | + }, []); | ||
| 41 | + | ||
| 32 | const handleUpdateRoomUser = useCallback((rawMessage: RawMessage) => { | 42 | const handleUpdateRoomUser = useCallback((rawMessage: RawMessage) => { |
| 33 | if (rawMessage.type == MessageType.ROOM_USER_UPDATE) { | 43 | if (rawMessage.type == MessageType.ROOM_USER_UPDATE) { |
| 34 | const data = rawMessage.message as UpdateRoomUser; | 44 | const data = rawMessage.message as UpdateRoomUser; |
| ... | @@ -81,8 +91,11 @@ export const Room: React.FC = () => { | ... | @@ -81,8 +91,11 @@ export const Room: React.FC = () => { |
| 81 | } | 91 | } |
| 82 | 92 | ||
| 83 | setRoomData(location.state.roomData); | 93 | setRoomData(location.state.roomData); |
| 94 | + socket.on('msg', handleInGame); | ||
| 84 | 95 | ||
| 85 | return () => { | 96 | return () => { |
| 97 | + socket.off('msg', handleInGame); | ||
| 98 | + | ||
| 86 | const rawMessage: RawMessage = { | 99 | const rawMessage: RawMessage = { |
| 87 | type: MessageType.ROOM_LEAVE, | 100 | type: MessageType.ROOM_LEAVE, |
| 88 | message: '' | 101 | message: '' |
| ... | @@ -94,12 +107,13 @@ export const Room: React.FC = () => { | ... | @@ -94,12 +107,13 @@ export const Room: React.FC = () => { |
| 94 | return ( | 107 | return ( |
| 95 | <Main> | 108 | <Main> |
| 96 | <RoomInfo roomData={roomData}/> | 109 | <RoomInfo roomData={roomData}/> |
| 110 | + <div className='w-full flex justify-center'> | ||
| 111 | + {/* 게임보드와 유저롤을 계속 살려둬서 리스너를 항상 열어놓도록 하자 */} | ||
| 112 | + <UserRole isInGame={isInGame} /> | ||
| 113 | + <GameBoard isInGame={isInGame} /> | ||
| 97 | { | 114 | { |
| 98 | isInGame ? ( | 115 | isInGame ? ( |
| 99 | - <div className='w-full flex'> | 116 | + <Chat w='w-4/12' h='h-132' /> |
| 100 | - <Canvas /> | ||
| 101 | - <Chat w='w-4/12' h='h-80' /> | ||
| 102 | - </div> | ||
| 103 | ) : ( | 117 | ) : ( |
| 104 | <div className='w-full flex flex-col justify-center items-center'> | 118 | <div className='w-full flex flex-col justify-center items-center'> |
| 105 | <UserInfo users={roomData.users}/> | 119 | <UserInfo users={roomData.users}/> |
| ... | @@ -108,6 +122,7 @@ export const Room: React.FC = () => { | ... | @@ -108,6 +122,7 @@ export const Room: React.FC = () => { |
| 108 | </div> | 122 | </div> |
| 109 | ) | 123 | ) |
| 110 | } | 124 | } |
| 125 | + </div> | ||
| 111 | </Main> | 126 | </Main> |
| 112 | ); | 127 | ); |
| 113 | } | 128 | } |
| ... | \ No newline at end of file | ... | \ No newline at end of file | ... | ... |
| ... | @@ -2,7 +2,18 @@ module.exports = { | ... | @@ -2,7 +2,18 @@ module.exports = { |
| 2 | purge: [], | 2 | purge: [], |
| 3 | darkMode: false, // or 'media' or 'class' | 3 | darkMode: false, // or 'media' or 'class' |
| 4 | theme: { | 4 | theme: { |
| 5 | - extend: {}, | 5 | + extend: { |
| 6 | + spacing: { | ||
| 7 | + 120: '30rem', | ||
| 8 | + 124: '31rem', | ||
| 9 | + 128: '32rem', | ||
| 10 | + 132: '33rem', | ||
| 11 | + 136: '34rem', | ||
| 12 | + 140: '35rem', | ||
| 13 | + 160: '40rem', | ||
| 14 | + 200: '50rem', | ||
| 15 | + }, | ||
| 16 | + }, | ||
| 6 | }, | 17 | }, |
| 7 | variants: { | 18 | variants: { |
| 8 | extend: { | 19 | extend: { | ... | ... |
-
Please register or login to post a comment