Overnap
Builds for 1 pipeline passed in 7 minutes 51 seconds

Merge branch 'feature/ingame' into develop

......@@ -25,6 +25,7 @@
"scripts": {
"start": "npm run twcss && set PORT=3001 && react-scripts start",
"build": "npm run twcss && react-scripts build",
"onlybuild": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject",
"twcss": "tailwind build src/tailwind.css -c tailwind.config.js -o src/index.css"
......
......@@ -18,5 +18,15 @@ export const MessageType = {
ROOM_USER_UPDATE: "updateRoomUser",
ROOM_CHAT: "chat",
ROOM_READY: "ready",
ROOM_START: "startGame"
ROOM_START: "startGame",
GAME_START: "startRound",
GAME_WORDSET: "wordSet",
GAME_CHOOSE: "chooseWord",
GAME_WORD: "wordChosen",
GAME_TIMER: "timer",
GAME_ACCEPT: "answerAccepted",
GAME_FINISH_ROUND: "finishRound",
GAME_FINISH_GAME: "finishGame",
DRAW_SET: "setBrush",
DRAW_MOVE: "moveBrush",
} as const
\ No newline at end of file
......
import React, { useCallback, useEffect, useRef, useState } from 'react';
import { Vector } from './types';
import React, { useCallback, useContext, useEffect, useRef, useState } from 'react';
import SocketContext from '../../contexts/SocketContext';
import { MessageType, RawMessage } from '../common/types';
import { BrushData, Vector } from './types';
// 참고 : https://basketdeveloper.tistory.com/79
export const Canvas: React.FC = () => {
interface CanvasProps {
isDrawer: boolean;
}
export const Canvas: React.FC<CanvasProps> = ({ isDrawer }) => {
const socket = useContext(SocketContext);
const canvasRef = useRef<HTMLCanvasElement>(null);
const [mousePosition, setMousePosition] = useState<Vector>({ x:0, y:0 });
......@@ -52,6 +59,22 @@ export const Canvas: React.FC = () => {
if (coordinates) {
setIsPainting(true);
setMousePosition(coordinates);
const rawMessage: RawMessage = {
type: MessageType.DRAW_MOVE,
message: coordinates
};
socket.emit('msg', rawMessage, () => {});
const nextRawMessage: RawMessage = {
type: MessageType.DRAW_SET,
message: {
size: 5,
color: '000000',
drawing: true
} as BrushData
};
socket.emit('msg', nextRawMessage, () => {});
}
}, []);
......@@ -65,6 +88,13 @@ export const Canvas: React.FC = () => {
const newMousePosition = getCoordinates(event);
if (mousePosition && newMousePosition) {
drawLine(mousePosition, newMousePosition);
const rawMessage: RawMessage = {
type: MessageType.DRAW_MOVE,
message: newMousePosition
};
socket.emit('msg', rawMessage, () => {});
setMousePosition(newMousePosition);
}
}
......@@ -73,11 +103,39 @@ export const Canvas: React.FC = () => {
);
const exitPaint = useCallback(() => {
const rawMessage: RawMessage = {
type: MessageType.DRAW_SET,
message: {
size: 5,
color: '000000',
drawing: false
} as BrushData
};
socket.emit('msg', rawMessage, () => {});
setIsPainting(false);
}, []);
const handleDrawSet = useCallback((rawMessage: RawMessage) => {
if (rawMessage.type === MessageType.DRAW_SET) {
const data = rawMessage.message as BrushData;
setIsPainting(data.drawing);
}
}, []);
const handleDrawMove = useCallback((rawMessage: RawMessage) => {
if (rawMessage.type === MessageType.DRAW_MOVE) {
const data = rawMessage.message as Vector;
if (isPainting) {
drawLine(mousePosition, data);
}
setMousePosition(data);
}
}, [isPainting, mousePosition])
useEffect(() => {
if (canvasRef.current) {
if (isDrawer) {
const canvas: HTMLCanvasElement = canvasRef.current;
canvas.addEventListener('mousedown', startPaint);
......@@ -92,11 +150,39 @@ export const Canvas: React.FC = () => {
canvas.removeEventListener('mouseleave', exitPaint);
};
}
}, [startPaint, paint, exitPaint]);
}
}, [isDrawer, startPaint, paint, exitPaint]);
useEffect(() => {
if (!isDrawer) {
socket.on('msg', handleDrawSet);
socket.on('msg', handleDrawMove);
return () => {
socket.off('msg', handleDrawSet);
socket.off('msg', handleDrawMove);
}
}
}, [isDrawer, handleDrawMove]);
const handleClearWhenStart = useCallback((rawMessage: RawMessage) => {
if (rawMessage.type === MessageType.GAME_START) {
clearCanvas();
setIsPainting(false);
// TODO: 펜 굵기, 색 설정하게 되면 여기에 초기화 넣기
}
}, []);
useEffect(() => {
socket.on('msg', handleClearWhenStart);
return () => {
socket.off('msg', handleClearWhenStart);
}
}, []);
return (
<div className='mx-3 px-2 py-1 rounded shadow'>
<canvas ref={canvasRef} width='512' height='384' />
<canvas ref={canvasRef} width='640' height='480' />
</div>
);
}
\ No newline at end of file
......
......@@ -23,9 +23,13 @@ export const Chat: React.FC<ChatProps> = (props) => {
}
socket.on('msg', handleChatData);
socket.on('msg', handleAcceptMessage);
socket.on('msg', handleFinishMessage);
return () => {
socket.off('msg', handleChatData);
socket.off('msg', handleAcceptMessage);
socket.on('msg', handleFinishMessage);
}
}, []);
......@@ -47,9 +51,29 @@ export const Chat: React.FC<ChatProps> = (props) => {
}
}, [input]);
const handleAcceptMessage = useCallback((rawMessage: RawMessage) => {
if (rawMessage.type === MessageType.GAME_ACCEPT) {
const message: ChatData = {
sender: 'SYSTEM',
message: 'That\'s correct!'
};
setChatLines(oldChatLines => [...oldChatLines, message]);
}
}, []);
const handleFinishMessage = useCallback((rawMessage: RawMessage) => {
if (rawMessage.type === MessageType.GAME_FINISH_ROUND) {
const message: ChatData = {
sender: 'SYSTEM',
message: 'The round is over!'
};
setChatLines(oldChatLines => [...oldChatLines, message]);
}
}, []);
return (
<div className={props.w}>
<div className={`${props.h} w-full rounded shadow flex flex-col overflow-y-scroll`}>
<div className={`${props.h} w-full py-2 rounded shadow flex flex-col overflow-y-scroll`}>
{chatLines.map((line, i) => (<ChatLine key={16383+i} chatData={line}/>))}
<div ref={messageEndRef} />
</div>
......
import React, { useCallback, useContext, useEffect, useState } from 'react';
import { useLocation } from 'react-router';
import SocketContext from '../../contexts/SocketContext';
import { MessageType, RawMessage } from '../common/types';
import { Canvas } from './Canvas';
import { RoundInfo } from './RoundInfo';
import { Role, RoundData } from './types';
import { Word } from './Word';
interface GameBoardLocation {
state: { username: string }
}
interface GameBoardProps {
isInGame: boolean
}
export const GameBoard: React.FC<GameBoardProps> = ({ isInGame }) => {
const socket = useContext(SocketContext);
const location: GameBoardLocation = useLocation();
const [ isDrawer, setIsDrawer ] = useState(false);
const [ words, setWords ] = useState<string[]>([]);
const [ wordChosen, setWordChosen ] = useState('');
const [ round, setRound ] = useState(0);
const handleWordSet = useCallback((rawMessage: RawMessage) => {
if (rawMessage.type === MessageType.GAME_WORDSET) {
console.log('단어 도착');
const { words } = rawMessage.message as { words: string[] };
setWords(words);
}
}, []);
const handleStart = useCallback((rawMessage: RawMessage) => {
if (rawMessage.type === MessageType.GAME_START) {
setWords([]);
const data = rawMessage.message as RoundData;
console.log('테스트 location ', location.state.username);
console.log('테스트 rolse ', data.roles);
const index = data.roles.findIndex(x => x.username === location.state.username);
setIsDrawer(data.roles[index].role === 'drawer');
setWordChosen('');
setRound(data.round);
}
}, []);
const handleGetWordLength = useCallback((rawMessage: RawMessage) => {
if (rawMessage.type === MessageType.GAME_WORD) {
if (wordChosen === '') {
const { length } = rawMessage.message as { length: number };
setWordChosen('_' + ' _'.repeat(length-1));
}
}
}, [wordChosen]);
const handleAnswer = useCallback((rawMessage: RawMessage) => {
if (rawMessage.type === MessageType.GAME_ACCEPT || rawMessage.type === MessageType.GAME_FINISH_ROUND) {
const { answer } = rawMessage.message as { answer: string };
setWordChosen(answer);
}
}, []);
useEffect(() => {
socket.on('msg', handleStart);
socket.on('msg', handleGetWordLength);
socket.on('msg', handleWordSet);
socket.on('msg', handleAnswer);
return () => {
socket.off('msg', handleStart);
socket.off('msg', handleGetWordLength);
socket.off('msg', handleWordSet);
socket.off('msg', handleAnswer);
}
}, []);
return (
<div className={`w-auto ${isInGame ? '' : 'hidden'}`}>
<div className='w-full flex flex-col justify-center items-center'>
{words.map((word, i) => (<Word key={word} index={i} word={word} setWordChosen={setWordChosen} setWords={setWords} />))}
</div>
<Canvas isDrawer={isDrawer && wordChosen !== ''} />
<RoundInfo round={round} wordChosen={wordChosen} />
</div>
);
}
\ No newline at end of file
......@@ -33,7 +33,7 @@ export const Ready: React.FC<ReadyProps> = ({ users }) => {
const handleReady = useCallback(() => {
if (isAdmin && isAllReady) {
const rawMessage: RawMessage = {
type: MessageType.ROOM_READY,
type: MessageType.ROOM_START,
message: {}
}
socket.emit('msg', rawMessage, () => {});
......
......@@ -10,7 +10,7 @@ interface RoomInfoProps {
export const RoomInfo: React.FC<RoomInfoProps> = ({ roomData }) => {
return (
<div className='m-3 mb-8 w-5/6 flex items-center place-content-between'>
<div className='m-3 my-8 w-5/6 flex items-center place-content-between'>
<div>{roomData.name}</div>
<div>{roomData.users.length}/{roomData.maxUsers}</div>
</div>
......
import React from 'react';
import SocketContext from '../../contexts/SocketContext';
import { Timer } from './Timer';
import { RoundData } from './types';
interface RoundInfoProps {
round: number;
wordChosen: string;
}
export const RoundInfo: React.FC<RoundInfoProps> = ({ round, wordChosen }) => {
return (
<div className='p-3 m-3 h-14 rounded shadow flex items-center place-content-between'>
<Timer />
<div>{wordChosen}</div>
<div>Round {round}/5</div>
</div>
);
}
\ No newline at end of file
import React, { useCallback, useContext, useEffect, useState } from 'react';
import SocketContext from '../../contexts/SocketContext';
import { MessageType, RawMessage } from '../common/types';
interface timer {
state: "started" | "stopped";
time: number;
};
export const Timer: React.FC = () => {
const socket = useContext(SocketContext);
const [ time, setTime ] = useState(0);
const [ isStop, setIsStop ] = useState(true);
const handleTimeSet = useCallback((rawMessage: RawMessage) => {
if (rawMessage.type === MessageType.GAME_TIMER) {
const data = rawMessage.message as timer;
console.log(data);
if (data.state === 'started') {
setIsStop(false);
} else {
setIsStop(true);
}
setTime(Math.floor(data.time));
}
}, []);
useEffect(() => {
if (!isStop) {
const go = setInterval(() => {
setTime(time-1);
}, 1000);
return () => clearInterval(go);
} else {
setTime(0);
}
}, [time, isStop]);
useEffect(() => {
socket.on('msg', handleTimeSet);
return () => {
socket.off('msg', handleTimeSet);
}
}, []);
return (
<div className={time < 10 ? 'text-red-500' : 'text-black'}>
🕒 {time}
</div>
);
}
\ No newline at end of file
import React, { useCallback, useContext, useEffect, useState } from 'react';
import SocketContext from '../../contexts/SocketContext';
import { MessageType, RawMessage } from '../common/types';
import { RoleData, RoundData } from './types';
interface UserRoleProps {
isInGame: boolean;
}
export const UserRole: React.FC<UserRoleProps> = ({ isInGame }) => {
const socket = useContext(SocketContext);
const [ roles, setRoles ] = useState<RoleData[]>([]);
const handleRole = useCallback((rawMessage: RawMessage) => {
if (rawMessage.type === MessageType.GAME_START) {
const { roles } = rawMessage.message as RoundData;
setRoles(roles);
}
}, []);
useEffect(() => {
socket.on('msg', handleRole);
return () => {
socket.off('msg', handleRole);
}
}, []);
return (
<div className={`w-40 h-140 rounded shadow flex flex-col items-center ${isInGame ? '' : 'hidden'}`}>
<div className='mt-3' />
{roles.map(x => (
<div key={x.username} className={`my-5 ease-linear transition-all duration-100
${x.role === 'drawer' ? 'text-blue-500'
: x.role === 'winner' ? 'text-green-500'
: 'text-black'}`}>
{x.nickname} {x.role === 'drawer' ? '🖌️' : x.role === 'spectator' ? '👻' : ''}
</div>
))}
</div>
);
}
import React, { useCallback, useContext } from 'react';
import { IndexType } from 'typescript';
import SocketContext from '../../contexts/SocketContext';
import { MessageResponse, MessageType, RawMessage } from '../common/types';
interface WordProps {
index: number;
word: string;
setWordChosen: (value: React.SetStateAction<string>) => void;
setWords: (value: React.SetStateAction<string[]>) => void;
}
export const Word: React.FC<WordProps> = (props) => {
const socket = useContext(SocketContext);
const handleChoose = useCallback(() => {
const rawMessage: RawMessage = {
type: MessageType.GAME_CHOOSE,
message: { word: props.word }
};
socket.emit('msg', rawMessage, (response: MessageResponse<undefined>) => {
if (response.ok) {
props.setWords([]);
props.setWordChosen(props.word);
}
});
}, [props.setWordChosen]);
return (
<button className={`bg-green-500 active:bg-green-600 fixed
text-white font-bold ${'mt-' + 40*(props.index+2)} mt-40
px-5 py-2 rounded shadow
outline-none focus:outline-none hover:shadow-md
ease-linear transition-all duration-100`}
type="button"
onClick={() => handleChoose()}>{props.word}</button>
);
}
......@@ -26,3 +26,23 @@ export interface Vector {
x: number;
y: number;
}
export type Role = "drawer" | "guesser" | "winner" | "spectator";
export interface RoleData {
username: string;
nickname: string;
role: Role;
}
export interface RoundData {
round: number;
duration: number;
roles: RoleData[];
};
export interface BrushData {
size: number;
color: string;
drawing: boolean;
}
......
......@@ -4,10 +4,12 @@ import { Main } from '../components/common/Main';
import { MessageResponse, MessageType, RawMessage } from '../components/common/types';
import { Canvas } from '../components/room/Canvas';
import { Chat } from '../components/room/Chat';
import { GameBoard } from '../components/room/GameBoard';
import { Ready } from '../components/room/Ready';
import { RoomInfo } from '../components/room/RoomInfo';
import { RoomData, UpdateRoomUser } from '../components/room/types';
import { UserInfo } from '../components/room/UserInfo';
import { UserRole } from '../components/room/UserRole';
import { UserStatus } from '../components/room/UserStatus';
import SocketContext from '../contexts/SocketContext';
......@@ -29,6 +31,14 @@ export const Room: React.FC = () => {
});
const [ isInGame, setIsInGame ] = useState(false);
const handleInGame = useCallback((rawMessage: RawMessage) => {
if (rawMessage.type === MessageType.GAME_START) {
setIsInGame(true);
} else if (rawMessage.type === MessageType.GAME_FINISH_GAME) {
setIsInGame(false);
}
}, []);
const handleUpdateRoomUser = useCallback((rawMessage: RawMessage) => {
if (rawMessage.type == MessageType.ROOM_USER_UPDATE) {
const data = rawMessage.message as UpdateRoomUser;
......@@ -81,8 +91,11 @@ export const Room: React.FC = () => {
}
setRoomData(location.state.roomData);
socket.on('msg', handleInGame);
return () => {
socket.off('msg', handleInGame);
const rawMessage: RawMessage = {
type: MessageType.ROOM_LEAVE,
message: ''
......@@ -94,12 +107,13 @@ export const Room: React.FC = () => {
return (
<Main>
<RoomInfo roomData={roomData}/>
<div className='w-full flex justify-center'>
{/* 게임보드와 유저롤을 계속 살려둬서 리스너를 항상 열어놓도록 하자 */}
<UserRole isInGame={isInGame} />
<GameBoard isInGame={isInGame} />
{
isInGame ? (
<div className='w-full flex'>
<Canvas />
<Chat w='w-4/12' h='h-80' />
</div>
<Chat w='w-4/12' h='h-132' />
) : (
<div className='w-full flex flex-col justify-center items-center'>
<UserInfo users={roomData.users}/>
......@@ -108,6 +122,7 @@ export const Room: React.FC = () => {
</div>
)
}
</div>
</Main>
);
}
\ No newline at end of file
......
......@@ -2,7 +2,18 @@ module.exports = {
purge: [],
darkMode: false, // or 'media' or 'class'
theme: {
extend: {},
extend: {
spacing: {
120: '30rem',
124: '31rem',
128: '32rem',
132: '33rem',
136: '34rem',
140: '35rem',
160: '40rem',
200: '50rem',
},
},
},
variants: {
extend: {
......