diff --git a/Caecae/package.json b/Caecae/package.json index cfc2a148..ed813ebb 100644 --- a/Caecae/package.json +++ b/Caecae/package.json @@ -30,8 +30,10 @@ "eslint-plugin-react-hooks": "^4.6.2", "eslint-plugin-react-refresh": "^0.4.7", "jest": "^29.7.0", + "terser-brunch": "^4.1.0", "ts-jest": "^29.2.3", "typescript": "^5.5.4", - "vite": "^5.4.0" + "vite": "^5.4.0", + "vite-plugin-compression": "^0.5.1" } } diff --git a/Caecae/public/assets/audio/racingGamePlayingSound.wav b/Caecae/public/assets/audio/racingGamePlayingSound.wav new file mode 100644 index 00000000..a53b0df6 Binary files /dev/null and b/Caecae/public/assets/audio/racingGamePlayingSound.wav differ diff --git a/Caecae/public/assets/audio/racingGameStopSound.wav b/Caecae/public/assets/audio/racingGameStopSound.wav new file mode 100644 index 00000000..573e82a0 Binary files /dev/null and b/Caecae/public/assets/audio/racingGameStopSound.wav differ diff --git a/Caecae/src/components/FindingGame/FindingGame.tsx b/Caecae/src/components/FindingGame/FindingGame.tsx index a55576e9..d8825b7a 100644 --- a/Caecae/src/components/FindingGame/FindingGame.tsx +++ b/Caecae/src/components/FindingGame/FindingGame.tsx @@ -1,6 +1,7 @@ import PictureGameBoard from "../common/PictureGameBoard/index.tsx"; import { action, + genrateFindGameAnswerCheckBodyParameter, initFindingGameState, } from "../../jobs/FindingGame/FindingGameWork.tsx"; import { useEffect, useRef } from "react"; @@ -8,48 +9,76 @@ import LottieContainer from "../common/LottieContainer/index.tsx"; import correctLottie from "@assets/animationCorrect.json"; import wrongLottie from "@assets/animationIncorrect.json"; import { store, useExistState } from "../../shared/Hyundux"; -//import HintSpot from "./Hint/HintSpot.tsx"; +import HintSpot from "./Hint/HintSpot.tsx"; import SmileBadge from "../common/SmileBadge/index.tsx"; import { createStory } from "../../shared/Hyundux-saga/Story.tsx"; import useSaga from "../../shared/Hyundux-saga/useSaga.tsx"; -import { getFindGameStory } from "../../stories/getFindingGame.tsx"; +import { getFindGameStory } from "../../stories/FindGame/getFindingGame.tsx"; +import { getFindGameAnswerStory } from "../../stories/FindGame/getFindGameIsAnswer.tsx"; +import { getFindGameHintStory } from "../../stories/FindGame/getFindGameHint.tsx"; const FindingGame = () => { const state = useExistState(initFindingGameState); - // const timerId = useRef(null); + const timerId = useRef(null); const [status, teller] = useSaga(); const pictureWidth = useRef(0); const pictureHeight = useRef(0); + const hintTime = 40 * 1000; // 1000ms는 1초 status; useEffect(() => { const getFindGameRunStory = createStory(getFindGameStory, {}); - teller(action.init, [getFindGameRunStory]); - // timerId.current = setTimeout(() => { - // store.dispatch(action.showHint()); - // }, 40000); + teller(action.init, getFindGameRunStory); + timerId.current = setTimeout(() => { + teller(action.showHint, getFindGameHintStory, { + answerList: state.showingAnswers.map((answer) => { + return { positionX: answer.positionX, positionY: answer.positionY }; + }), + }); + }, hintTime); }, []); - // useEffect(() => { - // if (state.showingHint.length == 0) { - // if (timerId.current != null) { - // clearInterval(timerId.current); - // } - // timerId.current = setTimeout(() => { - // store.dispatch(action.showHint()); - // }, 40000); - // } - // }, [state.showingHint]); + useEffect(() => { + if (timerId.current != null) { + clearInterval(timerId.current); + } + timerId.current = setTimeout(() => { + teller(action.showHint, getFindGameHintStory, { + answerList: state.showingAnswers.map((answer) => { + return { positionX: answer.positionX, positionY: answer.positionY }; + }), + }); + }, hintTime); + }, [state.showingAnswers]); + + useEffect(() => { + if (state.showingHint.length == 0) { + if (timerId.current != null) { + clearInterval(timerId.current); + } + timerId.current = setTimeout(() => { + teller(action.showHint, getFindGameHintStory, { + answerList: state.showingAnswers.map((answer) => { + return { positionX: answer.positionX, positionY: answer.positionY }; + }), + }); + }, hintTime); + } + }, [state.showingHint]); const onClickAction = ( width: number, - heigjht: number, + height: number, y: number, x: number ) => { pictureWidth.current = width; - pictureHeight.current = heigjht; + pictureHeight.current = height; if (state.gameStatus == "Gaming") { - store.dispatch(action.click(y, x, width, heigjht)); + teller( + action.click, + getFindGameAnswerStory, + genrateFindGameAnswerCheckBodyParameter(state, y, x, width, height) + ); } }; @@ -114,18 +143,26 @@ const FindingGame = () => { ); }); - // const showingHintElement = state.showingHint.map((hintAnswer) => { - // return ; - // }); - + const showingHintElement = state.showingHint.map((hintAnswer) => { + return ( + + ); + }); return (
{ + pictureWidth.current = width; + pictureHeight.current = height; + }} showingElements={[ ...showingCorrectElements, ...showingWrongElement, - //...showingHintElement, + ...showingHintElement, ...answerElement, ]} onClickAction={onClickAction} diff --git a/Caecae/src/components/RacingGame/RacingGame.tsx b/Caecae/src/components/RacingGame/RacingGame.tsx index 3e00a3a3..3ec8d589 100644 --- a/Caecae/src/components/RacingGame/RacingGame.tsx +++ b/Caecae/src/components/RacingGame/RacingGame.tsx @@ -6,121 +6,13 @@ import frontBackground from "@assets/frontBackground.svg"; import rearBackground from "@assets/rearBackground.svg"; import { action, - initRacingGameState + initRacingGameState, } from "../../jobs/RacingGame/RacingGameWork.tsx"; import { store, useExistState } from "../../shared/Hyundux/index.tsx"; import Link from "../../shared/Hyunouter/Link.tsx"; - -/** 게임 상태에 따라 다르게 보여지는 콘텐츠 */ -const gameContent = ( - gameStatus: string, - distance: number, - handlePlayGame: () => void, - enterEvent: () => void -) => { - switch (gameStatus) { - case "previous": - case "enterEvent": - return ( -
-
CASPER ELECTRIC
-
전력으로...!
-
- -
-
- ); - case "playing": - return ( -
-
Game Score
-
{distance.toFixed(3)} KM
-
-
stop :
-
- spacebarBtn -
-
-
- ); - case "end": - return ( -
-
-
Game Score
-
-
- {distance.toFixed(3)} KM -
-
상위 1%
-
-
-
- - -
-
- ); - default: - return null; - } -}; - -/** 게임 상태에 따라 다르게 보여지는 우측 상단 메뉴 */ -const gameMenu = (gameStatus: string) => { - switch (gameStatus) { - case "previous": - case "playing": - case "enterEvent": - return ( -
- - - -
- ); - case "end": - return ( -
- - - - -
- ); - case "enterEvent": - return ( -
- - - -
- ); - default: - return null; - } -}; +import getRacingGameTopRate from "../../stories/getRacingGameTopRate.tsx"; +import { useDebounce } from "../../hooks/index.tsx"; +import useAudio from "../../hooks/useAudio.tsx"; const RacingGame: React.FC = () => { const lottieRef = useRef(null); @@ -128,8 +20,17 @@ const RacingGame: React.FC = () => { const rearRef = useRef(null); const [frontBackgroundWidth, setFrontImageWidth] = useState(0); const [rearBackgroundWidth, setRearBackgroundWidth] = useState(0); - // const [state, dispatch] = useWork(initRacingGameState, racingGameReducer); const state = useExistState(initRacingGameState); + const [topRate, setTopRate] = useState(null); + const [isButtonDisabled, setIsButtonDisabled] = useState(false); + const debouncedDistance = useDebounce(state.distance, 50); + const { + audio: playingMusic, + playAudio: playingMusicPlay, + resetAudio: playingMusicReset, + } = useAudio("/assets/audio/racingGamePlayingSound.wav"); + const { playAudio: stopingMusicPlay, resetAudio: stopingMusicReset } = + useAudio("/assets/audio/racingGameStopSound.wav"); /** 모션 값을 사용하여 frontBackground의 x 위치 추적 */ const frontX = useMotionValue(0); @@ -138,6 +39,8 @@ const RacingGame: React.FC = () => { const frontAnimationControls = useAnimation(); const rearAnimationControls = useAnimation(); + const endGameTimeoutRef = useRef(null); + /** 이동한 km를 구하는 함수 */ const calculateDistance = (x: number) => { const totalDistance = Math.abs(x); @@ -145,8 +48,13 @@ const RacingGame: React.FC = () => { store.dispatch(action.updateDistance(totalDistance)); }; - /** 스페이스바를 눌렀을 때 멈추는 로직 */ const handleSmoothlyStop = () => { + const moveMoreDistance = 500; + + if (endGameTimeoutRef.current) { + clearTimeout(endGameTimeoutRef.current); + } + if (lottieRef.current) { lottieRef.current?.pause(); @@ -154,21 +62,20 @@ const RacingGame: React.FC = () => { const currentFrontX = frontRef.current?.getBoundingClientRect().x || 0; const currentRearX = rearRef.current?.getBoundingClientRect().x || 0; - /** 부드럽게 멈추는 로직 */ frontAnimationControls.start({ - x: currentFrontX - 500, // 현재 위치에서 500 만큼 더 이동 - transition: { duration: 1, ease: "easeOut" }, // 1초 동안 부드럽게 멈춤 + x: currentFrontX - moveMoreDistance, + transition: { duration: 1, ease: "easeOut" }, }); - /** 부드럽게 멈추는 로직 */ rearAnimationControls.start({ - x: currentRearX - 500, // 현재 위치에서 500 만큼 더 이동 - transition: { duration: 1, ease: "easeOut" }, // 1초 동안 부드럽게 멈춤 + x: currentRearX - moveMoreDistance, + transition: { duration: 1, ease: "easeOut" }, }); + + fadeOutStopingMusic(); } }; - /** 스페이스 바를 눌렀을 때 작동 로직 */ const handleSpacebar = (event: KeyboardEvent) => { if (event.code === "Space") { event.preventDefault(); @@ -178,8 +85,29 @@ const RacingGame: React.FC = () => { } }; - /** 게임 시작 시 작동 로직 */ + const fadeOutStopingMusic = () => { + const step = 0.1; + const duration = 1000; + const fadeInterval = duration / (playingMusic.volume / step); + + const fade = setInterval(() => { + if (playingMusic.volume > step) { + playingMusic.volume -= step; + } else { + clearInterval(fade); + playingMusicReset(); + stopingMusicPlay(); + } + }, fadeInterval); + }; + const handlePlayGame = () => { + setIsButtonDisabled(true); + setTopRate("?"); + + stopingMusicReset(); + playingMusicPlay(); + store.dispatch(action.gameStart()); if (lottieRef.current) { @@ -191,7 +119,7 @@ const RacingGame: React.FC = () => { transition: { duration: 7, repeat: 0 }, }) .then(() => { - lottieRef.current?.pause(); + handleSmoothlyStop(); store.dispatch(action.gameEnd()); }); @@ -199,14 +127,18 @@ const RacingGame: React.FC = () => { x: [0, -7000], transition: { duration: 7, repeat: 0 }, }); + + endGameTimeoutRef.current = setTimeout(() => { + handleSmoothlyStop(); + store.dispatch(action.gameEnd()); + }, 6000); } }; const enterEvent = () => { store.dispatch(action.enterEvent()); - } + }; - /** 2개의 백그라운드 이미지의 width를 구하는 로직 */ useEffect(() => { const frontBackgroundImg = new Image(); frontBackgroundImg.src = frontBackground; @@ -219,9 +151,16 @@ const RacingGame: React.FC = () => { rearBackgroundImg.onload = () => { setRearBackgroundWidth(rearBackgroundImg.width); }; + + return () => { + stopingMusicReset(); + playingMusicReset(); + if (endGameTimeoutRef.current) { + clearTimeout(endGameTimeoutRef.current); + } + }; }, []); - /** keydown 이벤트 리스너 등록 */ useEffect(() => { if (state.gameStatus === "playing") { document.addEventListener("keydown", handleSpacebar); @@ -242,6 +181,32 @@ const RacingGame: React.FC = () => { return () => unsubscribeFrontX(); }, [frontX]); + useEffect(() => { + const fetchData = async () => { + try { + const response = await getRacingGameTopRate(debouncedDistance); + setTopRate(response.data.percent.toFixed(3)); + } catch (error) { + console.error("레이싱 게임 점수 백분위 API 호출 오류:", error); + setTopRate(null); + } + }; + + if (debouncedDistance > 0 && state.gameStatus === "end") { + fetchData(); + } + }, [debouncedDistance]); + + useEffect(() => { + if (state.gameStatus === "end") { + const timer = setTimeout(() => { + setIsButtonDisabled(false); + }, 2000); + + return () => clearTimeout(timer); + } + }, [state.gameStatus]); + return (
{ autoplay={false} className="absolute top-[485px] left-[250px] w-[350px] h-auto z-[3]" /> - {gameContent(state.gameStatus, state.distance, handlePlayGame, enterEvent)} + {gameContent( + state.gameStatus, + state.distance, + topRate, + isButtonDisabled, + handlePlayGame, + enterEvent + )} {gameMenu(state.gameStatus)}
); }; -export default RacingGame; \ No newline at end of file +/** 게임 상태에 따라 다르게 보여지는 콘텐츠 */ +const gameContent = ( + gameStatus: string, + distance: number, + topRate: string | null, + isButtonDisabled: boolean, + handlePlayGame: () => void, + enterEvent: () => void +) => { + switch (gameStatus) { + case "previous": + case "enterEvent": + return ( +
+
+ CASPER ELECTRIC +
+
전력으로...!
+
+ +
+
+ ); + case "playing": + return ( +
+
+ Game Score +
+
+ {distance.toFixed(3)} KM +
+
+
+ stop : +
+
+ spacebarBtn +
+
+
+ ); + case "end": + return ( +
+
+
+ Game Score +
+
+
+ {distance.toFixed(3)} KM +
+
+ {`상위 ${topRate}%`} +
+
+
+
+ + +
+
+ ); + default: + return null; + } +}; + +/** 게임 상태에 따라 다르게 보여지는 우측 상단 메뉴 */ +const gameMenu = (gameStatus: string) => { + switch (gameStatus) { + case "previous": + case "playing": + case "enterEvent": + return ( +
+ + + +
+ ); + case "end": + return ( +
+ + + + +
+ ); + default: + return null; + } +}; + +export default RacingGame; diff --git a/Caecae/src/components/common/InfoSection/InfoSection.tsx b/Caecae/src/components/common/InfoSection/InfoSection.tsx index f667af7c..f039436f 100644 --- a/Caecae/src/components/common/InfoSection/InfoSection.tsx +++ b/Caecae/src/components/common/InfoSection/InfoSection.tsx @@ -34,7 +34,10 @@ const InfoSection = ({ }; return ( <> -
+
{header}
{children} diff --git a/Caecae/src/components/common/Navigation/Navigation.tsx b/Caecae/src/components/common/Navigation/Navigation.tsx index 96c1d962..e5f3dfac 100644 --- a/Caecae/src/components/common/Navigation/Navigation.tsx +++ b/Caecae/src/components/common/Navigation/Navigation.tsx @@ -30,7 +30,7 @@ const Navigation: React.FC = () => {