├── .husky └── pre-commit ├── .prettierignore ├── public ├── favicon.ico ├── logo192.png ├── logo512.png ├── robots.txt ├── manifest.json └── index.html ├── src ├── assets │ ├── sun.gif │ ├── birds.gif │ ├── clouds.gif │ ├── koopa.gif │ ├── mario.png │ ├── goombla.gif │ ├── mario-run.gif │ ├── audio │ │ ├── game-over.mp3 │ │ ├── mario-died.mp3 │ │ ├── mario-jump.mp3 │ │ └── running-about.mp3 │ ├── font │ │ └── Pixel_NES.otf │ └── brick.svg ├── components │ ├── Score.css │ ├── Sun.js │ ├── Footer.css │ ├── KeyMessage.css │ ├── KeyMessage.js │ ├── Sun.css │ ├── Title.js │ ├── Footer.js │ ├── Title.css │ ├── Bricks.css │ ├── Clouds.css │ ├── Birds.css │ ├── Bricks.js │ ├── Birds.js │ ├── Clouds.js │ ├── MobileControls.css │ ├── LoadingScreen.css │ ├── Mario.css │ ├── Score.js │ ├── LoadingScreen.js │ ├── Obstacles.css │ ├── MobileControls.js │ ├── Obstacles.js │ └── Mario.js ├── index.css ├── redux │ ├── store.js │ ├── engineSlice.js │ ├── marioSlice.js │ └── obstacleSlice.js ├── index.js ├── App.css └── App.js ├── .prettierrc ├── cypress.config.js ├── .gitignore ├── cypress ├── support │ ├── e2e.js │ └── commands.js └── e2e │ ├── home │ └── loading.cy.js │ └── gameplay │ └── play.cy.js ├── README.md ├── LICENSE ├── eslint.config.mjs ├── package.json └── .github └── workflows └── deployment.yml /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | npx lint-staged 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # Ignore artifacts: 2 | build 3 | coverage 4 | node_modules 5 | cypress -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/helloukey/mario-jump/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/helloukey/mario-jump/HEAD/public/logo192.png -------------------------------------------------------------------------------- /public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/helloukey/mario-jump/HEAD/public/logo512.png -------------------------------------------------------------------------------- /src/assets/sun.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/helloukey/mario-jump/HEAD/src/assets/sun.gif -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /src/assets/birds.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/helloukey/mario-jump/HEAD/src/assets/birds.gif -------------------------------------------------------------------------------- /src/assets/clouds.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/helloukey/mario-jump/HEAD/src/assets/clouds.gif -------------------------------------------------------------------------------- /src/assets/koopa.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/helloukey/mario-jump/HEAD/src/assets/koopa.gif -------------------------------------------------------------------------------- /src/assets/mario.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/helloukey/mario-jump/HEAD/src/assets/mario.png -------------------------------------------------------------------------------- /src/assets/goombla.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/helloukey/mario-jump/HEAD/src/assets/goombla.gif -------------------------------------------------------------------------------- /src/assets/mario-run.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/helloukey/mario-jump/HEAD/src/assets/mario-run.gif -------------------------------------------------------------------------------- /src/assets/audio/game-over.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/helloukey/mario-jump/HEAD/src/assets/audio/game-over.mp3 -------------------------------------------------------------------------------- /src/assets/audio/mario-died.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/helloukey/mario-jump/HEAD/src/assets/audio/mario-died.mp3 -------------------------------------------------------------------------------- /src/assets/audio/mario-jump.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/helloukey/mario-jump/HEAD/src/assets/audio/mario-jump.mp3 -------------------------------------------------------------------------------- /src/assets/font/Pixel_NES.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/helloukey/mario-jump/HEAD/src/assets/font/Pixel_NES.otf -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "es5", 3 | "tabWidth": 2, 4 | "semi": true, 5 | "singleQuote": false 6 | } 7 | -------------------------------------------------------------------------------- /src/assets/audio/running-about.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/helloukey/mario-jump/HEAD/src/assets/audio/running-about.mp3 -------------------------------------------------------------------------------- /src/components/Score.css: -------------------------------------------------------------------------------- 1 | .score { 2 | font-size: 1.2rem; 3 | margin: 0.5rem; 4 | color: #eee; 5 | } 6 | 7 | /* Media Queries */ 8 | @media (max-width: 640px) { 9 | .score { 10 | font-size: 1rem; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/components/Sun.js: -------------------------------------------------------------------------------- 1 | import "./Sun.css"; 2 | 3 | const Sun = () => { 4 | return ( 5 |
6 |
7 |
8 | ); 9 | }; 10 | export default Sun; 11 | -------------------------------------------------------------------------------- /cypress.config.js: -------------------------------------------------------------------------------- 1 | const { defineConfig } = require("cypress"); 2 | 3 | module.exports = defineConfig({ 4 | e2e: { 5 | setupNodeEvents(on, config) { 6 | // implement node event listeners here 7 | }, 8 | baseUrl: "http://localhost:3000", 9 | }, 10 | }); 11 | -------------------------------------------------------------------------------- /src/components/Footer.css: -------------------------------------------------------------------------------- 1 | .copyright { 2 | width: 100%; 3 | font-size: 0.8rem; 4 | text-align: center; 5 | margin: auto; 6 | position: fixed; 7 | bottom: 20px; 8 | color: #eee; 9 | z-index: 100; 10 | } 11 | 12 | .copyright-link { 13 | color: #fbd000; 14 | text-decoration: none; 15 | } 16 | -------------------------------------------------------------------------------- /src/components/KeyMessage.css: -------------------------------------------------------------------------------- 1 | .press-container { 2 | width: 100%; 3 | position: absolute; 4 | top: 50%; 5 | left: 50%; 6 | transform: translate(-50%, -50%); 7 | font-size: 1.5rem; 8 | color: #eee; 9 | text-align: center; 10 | } 11 | 12 | /* Media Queries */ 13 | @media (max-width: 1024px) { 14 | .press-container { 15 | font-size: 1rem; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: "pixelNES"; 3 | src: url("./assets/font/Pixel_NES.otf"); 4 | } 5 | 6 | * { 7 | box-sizing: border-box; 8 | margin: 0; 9 | padding: 0; 10 | font-family: pixelNES, Arial, Helvetica, sans-serif; 11 | } 12 | 13 | body { 14 | height: 100vh; 15 | width: 100%; 16 | overflow-y: hidden; 17 | background-color: #2d2d2d; 18 | } 19 | -------------------------------------------------------------------------------- /src/redux/store.js: -------------------------------------------------------------------------------- 1 | import { configureStore } from "@reduxjs/toolkit"; 2 | import marioReducer from "./marioSlice"; 3 | import obstacleReducer from "./obstacleSlice"; 4 | import engineReducer from "./engineSlice"; 5 | 6 | export const store = configureStore({ 7 | reducer: { 8 | mario: marioReducer, 9 | obstacle: obstacleReducer, 10 | engine: engineReducer, 11 | }, 12 | }); 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /src/components/KeyMessage.js: -------------------------------------------------------------------------------- 1 | import "./KeyMessage.css"; 2 | 3 | const PressAnyKey = () => { 4 | return ( 5 |
6 |

7 | ENTER KEY - START GAME 8 |

9 |

10 | SPACE KEY - JUMP! 11 |

12 |
13 | ); 14 | }; 15 | export default PressAnyKey; 16 | -------------------------------------------------------------------------------- /src/components/Sun.css: -------------------------------------------------------------------------------- 1 | .sun { 2 | background-image: url("../assets/sun.gif"); 3 | background-repeat: no-repeat; 4 | background-size: contain; 5 | height: 100%; 6 | width: 100px; 7 | position: absolute; 8 | top: 25px; 9 | right: 100px; 10 | } 11 | 12 | /* Media Queries */ 13 | @media (max-width: 1280px) { 14 | .sun { 15 | width: 80px; 16 | } 17 | } 18 | 19 | @media (max-width: 1024px) { 20 | .sun { 21 | right: 50px; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/components/Title.js: -------------------------------------------------------------------------------- 1 | import "./Title.css"; 2 | 3 | // assets 4 | import Mario from "../assets/mario.png"; 5 | 6 | const Title = () => { 7 | return ( 8 |
9 | 15 |

16 | Mario Jump 17 |

18 |
19 | ); 20 | }; 21 | export default Title; 22 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom/client"; 3 | import "./index.css"; 4 | import App from "./App"; 5 | import { Provider } from "react-redux"; 6 | import { store } from "./redux/store"; 7 | import Title from "./components/Title"; 8 | 9 | const root = ReactDOM.createRoot(document.getElementById("root")); 10 | root.render( 11 | 12 | 13 | 14 | <App /> 15 | </Provider> 16 | </React.StrictMode> 17 | ); 18 | -------------------------------------------------------------------------------- /src/components/Footer.js: -------------------------------------------------------------------------------- 1 | import "./Footer.css"; 2 | 3 | const Footer = () => { 4 | return ( 5 | <div className="copyright" data-cy="footer-copyright-div"> 6 | Copyright © {new Date().getFullYear()}{" "} 7 | <a 8 | href="https://github.com/helloukey" 9 | target="_blank" 10 | rel="noreferrer" 11 | className="copyright-link" 12 | data-cy="footer-copyright-link" 13 | > 14 | Kunal Ukey 15 | </a> 16 | </div> 17 | ); 18 | }; 19 | export default Footer; 20 | -------------------------------------------------------------------------------- /src/components/Title.css: -------------------------------------------------------------------------------- 1 | .title-container { 2 | display: flex; 3 | justify-content: center; 4 | align-items: center; 5 | gap: 50px; 6 | margin: 25px 0px 0px 0px; 7 | } 8 | 9 | .mario-logo { 10 | width: 80px; 11 | height: auto; 12 | } 13 | 14 | .title { 15 | font-size: 3rem; 16 | color: #e52521; 17 | } 18 | 19 | /* Media Queries */ 20 | @media (max-width: 640px) { 21 | .title-container { 22 | gap: 25px; 23 | } 24 | .mario-logo { 25 | width: 50px; 26 | height: auto; 27 | } 28 | .title { 29 | font-size: 1.5rem; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /cypress/support/e2e.js: -------------------------------------------------------------------------------- 1 | // *********************************************************** 2 | // This example support/e2e.js is processed and 3 | // loaded automatically before your test files. 4 | // 5 | // This is a great place to put global configuration and 6 | // behavior that modifies Cypress. 7 | // 8 | // You can change the location of this file or turn off 9 | // automatically serving support files with the 10 | // 'supportFile' configuration option. 11 | // 12 | // You can read more here: 13 | // https://on.cypress.io/configuration 14 | // *********************************************************** 15 | 16 | // Import commands.js using ES2015 syntax: 17 | import './commands' 18 | 19 | // Alternatively you can use CommonJS syntax: 20 | // require('./commands') -------------------------------------------------------------------------------- /src/components/Bricks.css: -------------------------------------------------------------------------------- 1 | .brick { 2 | background-image: url("../assets/brick.svg"); 3 | background-repeat: repeat-x; 4 | background-size: contain; 5 | height: 120px; 6 | width: 1000vw; 7 | position: absolute; 8 | bottom: 0px; 9 | left: 0px; 10 | } 11 | 12 | .brick-animate { 13 | animation-name: bricks; 14 | animation-duration: 5s; 15 | animation-timing-function: linear; 16 | animation-iteration-count: infinite; 17 | } 18 | 19 | @keyframes bricks { 20 | 0% { 21 | left: 0px; 22 | } 23 | 100% { 24 | left: -1200px; 25 | } 26 | } 27 | 28 | /* Media Queries */ 29 | @media (max-width: 1280px) { 30 | .brick { 31 | height: 100px; 32 | } 33 | } 34 | 35 | @media (max-width: 1024px) { 36 | .brick { 37 | height: 60px; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/App.css: -------------------------------------------------------------------------------- 1 | .App { 2 | max-width: 1280px; 3 | width: 100%; 4 | height: 70vh; 5 | margin: auto; 6 | border: 0.5rem solid #eee; 7 | position: absolute; 8 | top: 50%; 9 | left: 50%; 10 | transform: translate(-50%, -50%); 11 | background: rgb(255, 134, 134); 12 | background: linear-gradient( 13 | 0deg, 14 | rgba(255, 134, 134, 1) 0%, 15 | rgba(2, 233, 242, 1) 0%, 16 | rgba(44, 127, 247, 1) 100% 17 | ); 18 | overflow: hidden; 19 | user-select: none; 20 | pointer-events: none; 21 | } 22 | 23 | /* Media Queries */ 24 | @media (max-width: 1280px) { 25 | .App { 26 | width: 95%; 27 | border: 0.2rem solid #eee; 28 | } 29 | } 30 | 31 | @media (max-width: 1024px) { 32 | .App { 33 | height: 50vh; 34 | top: 40%; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MARIO JUMP 2 | 3 | ![mario header](https://user-images.githubusercontent.com/43317360/192131459-0b53fe51-b650-437d-af6a-5d24349a6e0d.png) **Mario Jump** is an endless running game inspired by the famous **Google Chrome's T-Rex Dinosaur Game 🦖**. 4 | The game is built entirely with ReactJS ⚛ and Redux Toolkit 🔧. 5 | Play Here: **[Mario Jump](https://helloukey.github.io/mario-jump)** 6 | 7 | ## Gameplay 8 | 9 | ![gameplay gif](https://user-images.githubusercontent.com/43317360/192130942-51b6f545-33a9-4a4f-bfe1-7c3418380746.gif) 10 | 11 | ## Features 12 | 13 | - Mobile Responsive 📱 14 | - Beautiful Animations ☄ 15 | - Rapid Controls 🎮 16 | - High Score 💯 17 | - Mario Background Music ❤ 18 | - Jump & Game Over Sound Effect 🔊 19 | 20 | ## License 21 | 22 | [MIT](LICENSE) 23 | -------------------------------------------------------------------------------- /src/components/Clouds.css: -------------------------------------------------------------------------------- 1 | .clouds { 2 | background-image: url("../assets/clouds.gif"); 3 | background-repeat: no-repeat; 4 | background-size: contain; 5 | height: 120px; 6 | width: 100%; 7 | position: absolute; 8 | top: 20px; 9 | left: 100vw; 10 | } 11 | 12 | .clouds-animate { 13 | animation-name: clouds-animation; 14 | animation-duration: 60s; 15 | animation-timing-function: linear; 16 | animation-iteration-count: infinite; 17 | } 18 | 19 | @keyframes clouds-animation { 20 | 0% { 21 | left: 100vw; 22 | } 23 | 100% { 24 | left: -15vw; 25 | } 26 | } 27 | 28 | /* Media Queries */ 29 | @media (max-width: 1024px) { 30 | .clouds { 31 | height: 100px; 32 | } 33 | } 34 | 35 | @media (max-width: 640px) { 36 | .clouds { 37 | height: 80px; 38 | top: 25px; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/components/Birds.css: -------------------------------------------------------------------------------- 1 | .birds { 2 | background-image: url("../assets/birds.gif"); 3 | background-repeat: no-repeat; 4 | background-size: contain; 5 | height: 100%; 6 | width: 100px; 7 | position: absolute; 8 | top: 150px; 9 | left: 100vw; 10 | } 11 | 12 | .birds-animate { 13 | animation-name: birds-animation; 14 | animation-duration: 30s; 15 | animation-timing-function: linear; 16 | animation-iteration-count: infinite; 17 | } 18 | 19 | @keyframes birds-animation { 20 | 0% { 21 | left: 100vw; 22 | } 23 | 100% { 24 | left: -15vw; 25 | } 26 | } 27 | 28 | /* Media Queries */ 29 | @media (max-width: 1024px) { 30 | .birds { 31 | width: 80px; 32 | top: 125px; 33 | } 34 | } 35 | 36 | @media (max-width: 640px) { 37 | .birds { 38 | width: 60px; 39 | top: 100px; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/components/Bricks.js: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | import "./Bricks.css"; 3 | 4 | const Bricks = () => { 5 | const [isReady, setIsReady] = useState(false); 6 | 7 | // Check if document is loaded before animating clouds 8 | useEffect(() => { 9 | const setLoad = () => setIsReady(true); 10 | 11 | if (document.readyState === "complete") { 12 | setLoad(); 13 | } else { 14 | window.addEventListener("load", setLoad); 15 | 16 | // return cleanup function 17 | return () => window.removeEventListener("load", setLoad); 18 | } 19 | }, []); 20 | return ( 21 | <div className="bricks-container"> 22 | <div 23 | className={isReady ? "brick brick-animate" : "brick"} 24 | data-cy="brick" 25 | /> 26 | </div> 27 | ); 28 | }; 29 | export default Bricks; 30 | -------------------------------------------------------------------------------- /src/components/Birds.js: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | import "./Birds.css"; 3 | 4 | const Birds = () => { 5 | const [isReady, setIsReady] = useState(false); 6 | 7 | // check if document is loaded before animating brids 8 | useEffect(() => { 9 | const setLoad = () => setIsReady(true); 10 | 11 | if (document.readyState === "complete") { 12 | setLoad(); 13 | } else { 14 | window.addEventListener("load", setLoad); 15 | 16 | // return cleanup function 17 | return () => window.removeEventListener("load", setLoad); 18 | } 19 | }, []); 20 | 21 | return ( 22 | <div className="birds-container"> 23 | <div 24 | className={isReady ? "birds birds-animate" : "birds"} 25 | data-cy="birds" 26 | ></div> 27 | </div> 28 | ); 29 | }; 30 | export default Birds; 31 | -------------------------------------------------------------------------------- /src/components/Clouds.js: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | import "./Clouds.css"; 3 | 4 | const Clouds = () => { 5 | const [isReady, setIsReady] = useState(false); 6 | 7 | // Check if document is loaded before animating clouds 8 | useEffect(() => { 9 | const setLoad = () => setIsReady(true); 10 | 11 | if (document.readyState === "complete") { 12 | setLoad(); 13 | } else { 14 | window.addEventListener("load", setLoad); 15 | 16 | // return cleanup function 17 | return () => window.removeEventListener("load", setLoad); 18 | } 19 | }, []); 20 | 21 | return ( 22 | <div className="clouds-container"> 23 | <div 24 | className={isReady ? "clouds clouds-animate" : "clouds"} 25 | data-cy="clouds" 26 | ></div> 27 | </div> 28 | ); 29 | }; 30 | export default Clouds; 31 | -------------------------------------------------------------------------------- /src/components/MobileControls.css: -------------------------------------------------------------------------------- 1 | @media (min-width: 1024px) { 2 | .mobile-controls-container, 3 | .control-start-button, 4 | .control-jump-button { 5 | display: none; 6 | } 7 | } 8 | 9 | .mobile-controls-container { 10 | display: flex; 11 | flex-direction: column; 12 | justify-content: center; 13 | align-items: center; 14 | position: absolute; 15 | left: 50%; 16 | bottom: 15vh; 17 | transform: translateX(-50%); 18 | margin: auto; 19 | } 20 | 21 | .control-start-button, 22 | .control-jump-button, 23 | .control-die-button { 24 | font-size: 2rem; 25 | padding: 0.5rem 2rem; 26 | border: none; 27 | border-radius: 0.2rem; 28 | cursor: pointer; 29 | box-shadow: rgba(149, 157, 165, 0.2) 0px 8px 24px; 30 | user-select: none; 31 | } 32 | 33 | .control-start-button:hover, 34 | .control-jump-button:hover { 35 | filter: saturate(10); 36 | } 37 | 38 | .control-start-button, 39 | .control-die-button { 40 | background-color: #e52521; 41 | } 42 | 43 | .control-jump-button { 44 | background-color: #fbd000; 45 | } 46 | -------------------------------------------------------------------------------- /src/components/LoadingScreen.css: -------------------------------------------------------------------------------- 1 | .loading-screen-container { 2 | width: 100%; 3 | height: 100vh; 4 | position: fixed; 5 | top: 0; 6 | left: 0; 7 | bottom: 0; 8 | right: 0; 9 | background-color: #2d2d2d; 10 | color: #eee; 11 | display: flex; 12 | flex-direction: column; 13 | justify-content: center; 14 | align-items: center; 15 | gap: 2rem; 16 | z-index: 10; 17 | } 18 | 19 | .loading-mario { 20 | width: 100px; 21 | height: auto; 22 | } 23 | 24 | .loading-title { 25 | font-size: 2rem; 26 | text-align: center; 27 | padding: 0.5rem 2rem; 28 | background-color: #e52521; 29 | border-radius: 0.2rem; 30 | box-shadow: rgba(149, 157, 165, 0.2) 0px 8px 24px; 31 | } 32 | 33 | .enter-button { 34 | font-size: 2rem; 35 | text-align: center; 36 | padding: 0.5rem 2rem; 37 | background-color: #fbd000; 38 | color: #2d2d2d; 39 | border-radius: 0.2rem; 40 | border: none; 41 | box-shadow: rgba(149, 157, 165, 0.2) 0px 8px 24px; 42 | cursor: pointer; 43 | } 44 | 45 | .enter-button:hover { 46 | filter: saturate(10); 47 | } 48 | -------------------------------------------------------------------------------- /src/components/Mario.css: -------------------------------------------------------------------------------- 1 | .mario { 2 | width: 5rem; 3 | height: auto; 4 | position: absolute; 5 | bottom: 12.4vh; 6 | left: 40px; 7 | } 8 | 9 | .jump { 10 | animation-name: jumping; 11 | animation-timing-function: ease-out; 12 | animation-duration: 0.4s; 13 | } 14 | 15 | @keyframes jumping { 16 | 0% { 17 | transform: translateY(-15vh); 18 | } 19 | } 20 | 21 | /* Mario Die */ 22 | .die { 23 | animation-name: die-animation; 24 | animation-timing-function: ease-in; 25 | animation-duration: 2s; 26 | } 27 | 28 | @keyframes die-animation { 29 | 25% { 30 | bottom: 30vh; 31 | left: 8vh; 32 | } 33 | 100% { 34 | bottom: -20vh; 35 | } 36 | } 37 | 38 | /* Media Queries */ 39 | @media (max-width: 1280px) { 40 | .mario { 41 | width: 4rem; 42 | bottom: 100px; 43 | } 44 | } 45 | 46 | @media (max-width: 1024px) { 47 | .mario { 48 | width: 3rem; 49 | bottom: 60px; 50 | } 51 | } 52 | 53 | @media (max-width: 640px) { 54 | .mario { 55 | width: 2rem; 56 | left: 20px; 57 | bottom: 60px; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /cypress/support/commands.js: -------------------------------------------------------------------------------- 1 | // *********************************************** 2 | // This example commands.js shows you how to 3 | // create various custom commands and overwrite 4 | // existing commands. 5 | // 6 | // For more comprehensive examples of custom 7 | // commands please read more here: 8 | // https://on.cypress.io/custom-commands 9 | // *********************************************** 10 | // 11 | // 12 | // -- This is a parent command -- 13 | // Cypress.Commands.add('login', (email, password) => { ... }) 14 | // 15 | // 16 | // -- This is a child command -- 17 | // Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... }) 18 | // 19 | // 20 | // -- This is a dual command -- 21 | // Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... }) 22 | // 23 | // 24 | // -- This will overwrite an existing command -- 25 | // Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... }) 26 | 27 | Cypress.Commands.add( 28 | "getCypress", 29 | (attribute) => { 30 | return cy.get(`[data-cy=${attribute}]`); 31 | } 32 | ); -------------------------------------------------------------------------------- /src/redux/engineSlice.js: -------------------------------------------------------------------------------- 1 | import { createSlice } from "@reduxjs/toolkit"; 2 | 3 | const initialState = { 4 | play: false, 5 | die: false, 6 | speed: 0, 7 | score: 0, 8 | lastScore: 0, 9 | loadingScreen: true, 10 | }; 11 | 12 | export const engineSlice = createSlice({ 13 | name: "engine", 14 | initialState, 15 | reducers: { 16 | setReady: (state, action) => { 17 | state.play = action.payload; 18 | }, 19 | setDie: (state, action) => { 20 | state.die = action.payload; 21 | }, 22 | setSpeed: (state, action) => { 23 | state.speed += action.payload; 24 | }, 25 | setScore: (state, action) => { 26 | state.score = action.payload; 27 | }, 28 | setLastScore: (state, action) => { 29 | state.lastScore = action.payload; 30 | }, 31 | setLoadingScreen: (state, action) => { 32 | state.loadingScreen = action.payload; 33 | }, 34 | }, 35 | }); 36 | 37 | export const { 38 | setReady, 39 | setDie, 40 | setSpeed, 41 | setScore, 42 | setLastScore, 43 | setLoadingScreen, 44 | } = engineSlice.actions; 45 | export default engineSlice.reducer; 46 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Kunal Ukey 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import "./App.css"; 2 | import Birds from "./components/Birds"; 3 | import Bricks from "./components/Bricks"; 4 | import Clouds from "./components/Clouds"; 5 | import Mario from "./components/Mario"; 6 | import Obstacles from "./components/Obstacles"; 7 | import Sun from "./components/Sun"; 8 | import KeyMessage from "./components/KeyMessage"; 9 | import LoadingScreen from "./components/LoadingScreen"; 10 | 11 | // redux 12 | import { useSelector } from "react-redux"; 13 | import Score from "./components/Score"; 14 | import MobileControls from "./components/MobileControls"; 15 | import Footer from "./components/Footer"; 16 | 17 | function App() { 18 | const isPlay = useSelector((state) => state.engine.play); 19 | const isLoading = useSelector((state) => state.engine.loadingScreen); 20 | return ( 21 | <> 22 | {isLoading && <LoadingScreen />} 23 | <div className="App"> 24 | {!isPlay && <KeyMessage />} 25 | <Bricks /> 26 | <Mario /> 27 | <Sun /> 28 | <Clouds /> 29 | <Birds /> 30 | <Obstacles /> 31 | <Score /> 32 | </div> 33 | <MobileControls /> 34 | <Footer /> 35 | </> 36 | ); 37 | } 38 | 39 | export default App; 40 | -------------------------------------------------------------------------------- /src/components/Score.js: -------------------------------------------------------------------------------- 1 | import { useEffect } from "react"; 2 | import { useDispatch, useSelector } from "react-redux"; 3 | import { setScore, setLastScore } from "../redux/engineSlice"; 4 | import "./Score.css"; 5 | 6 | const Score = () => { 7 | const score = useSelector((state) => state.engine.score); 8 | const lastScore = useSelector((state) => state.engine.lastScore); 9 | const play = useSelector((state) => state.engine.play); 10 | const die = useSelector((state) => state.engine.die); 11 | const dispatch = useDispatch(); 12 | 13 | useEffect(() => { 14 | if (play && !die) { 15 | setTimeout(() => { 16 | dispatch(setScore(score + 1)); 17 | }, 100); 18 | } 19 | if (score && !play) { 20 | dispatch(setLastScore(score)); 21 | } 22 | }, [dispatch, play, score, lastScore, die]); 23 | return ( 24 | <div className="score-container"> 25 | {play && ( 26 | <p className="score" data-cy="score-text"> 27 | Score: {score} 28 | </p> 29 | )} 30 | {!play && ( 31 | <p className="score" data-cy="last-score-text"> 32 | Score: {lastScore} 33 | </p> 34 | )} 35 | </div> 36 | ); 37 | }; 38 | export default Score; 39 | -------------------------------------------------------------------------------- /src/components/LoadingScreen.js: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | import MarioCharacter from "../assets/mario.png"; 3 | import "./LoadingScreen.css"; 4 | import { setLoadingScreen } from "../redux/engineSlice"; 5 | import { useDispatch } from "react-redux"; 6 | 7 | const LoadingScreen = () => { 8 | const [isReady, setIsReady] = useState(false); 9 | const dispatch = useDispatch(); 10 | 11 | useEffect(() => { 12 | setTimeout(() => { 13 | setIsReady(true); 14 | }, 5000); 15 | }, []); 16 | 17 | return ( 18 | <div className="loading-screen-container"> 19 | <img 20 | src={MarioCharacter} 21 | alt="" 22 | className="loading-mario" 23 | data-cy="loading-mario" 24 | /> 25 | {!isReady && ( 26 | <h1 className="loading-title" data-cy="loading-loading-text"> 27 | Loading... 28 | </h1> 29 | )} 30 | {isReady && ( 31 | <button 32 | className="enter-button" 33 | onClick={() => dispatch(setLoadingScreen(false))} 34 | data-cy="loading-enter-button" 35 | > 36 | ENTER 37 | </button> 38 | )} 39 | </div> 40 | ); 41 | }; 42 | export default LoadingScreen; 43 | -------------------------------------------------------------------------------- /src/redux/marioSlice.js: -------------------------------------------------------------------------------- 1 | import { createSlice } from "@reduxjs/toolkit"; 2 | 3 | const initialState = { 4 | jumping: false, 5 | bottom: null, 6 | height: null, 7 | left: null, 8 | right: null, 9 | top: null, 10 | width: null, 11 | x: null, 12 | y: null, 13 | }; 14 | 15 | export const marioSlice = createSlice({ 16 | name: "mario", 17 | initialState, 18 | reducers: { 19 | marioJumping: (state, action) => { 20 | state.jumping = action.payload; 21 | }, 22 | marioBottom: (state, action) => { 23 | state.bottom = action.payload; 24 | }, 25 | marioHeight: (state, action) => { 26 | state.height = action.payload; 27 | }, 28 | marioLeft: (state, action) => { 29 | state.left = action.payload; 30 | }, 31 | marioRight: (state, action) => { 32 | state.right = action.payload; 33 | }, 34 | marioTop: (state, action) => { 35 | state.top = action.payload; 36 | }, 37 | marioWidth: (state, action) => { 38 | state.width = action.payload; 39 | }, 40 | marioX: (state, action) => { 41 | state.x = action.payload; 42 | }, 43 | marioY: (state, action) => { 44 | state.y = action.payload; 45 | }, 46 | }, 47 | }); 48 | 49 | export const { 50 | marioJumping, 51 | marioBottom, 52 | marioHeight, 53 | marioLeft, 54 | marioRight, 55 | marioTop, 56 | marioWidth, 57 | marioX, 58 | marioY, 59 | } = marioSlice.actions; 60 | export default marioSlice.reducer; 61 | -------------------------------------------------------------------------------- /src/components/Obstacles.css: -------------------------------------------------------------------------------- 1 | .obstacles-container { 2 | width: 100vw; 3 | height: auto; 4 | display: flex; 5 | justify-content: space-between; 6 | align-items: center; 7 | } 8 | 9 | .obstacle1, 10 | .obstacle2 { 11 | height: 60px; 12 | width: auto; 13 | position: absolute; 14 | bottom: 120px; 15 | } 16 | 17 | .obstacle1 { 18 | left: 120vw; 19 | } 20 | .obstacle2 { 21 | left: 240vw; 22 | } 23 | 24 | .obstacle1-move { 25 | animation-name: obstacle1-animation; 26 | animation-duration: 3s; 27 | animation-timing-function: linear; 28 | animation-iteration-count: infinite; 29 | } 30 | 31 | .obstacle2-move { 32 | animation-name: obstacle2-animation; 33 | animation-duration: 6s; 34 | animation-timing-function: linear; 35 | animation-iteration-count: infinite; 36 | } 37 | 38 | @keyframes obstacle1-animation { 39 | 0% { 40 | left: 125vw; 41 | } 42 | 100% { 43 | left: -120vw; 44 | } 45 | } 46 | 47 | @keyframes obstacle2-animation { 48 | 0% { 49 | left: 240vw; 50 | } 51 | 100% { 52 | left: -240vw; 53 | } 54 | } 55 | 56 | /* Media Queries */ 57 | @media (max-width: 1280px) { 58 | .obstacle1, 59 | .obstacle2 { 60 | height: 50px; 61 | bottom: 100px; 62 | } 63 | } 64 | 65 | @media (max-width: 1024px) { 66 | .obstacle1, 67 | .obstacle2 { 68 | height: 40px; 69 | bottom: 60px; 70 | } 71 | } 72 | 73 | @media (max-width: 640px) { 74 | .obstacle1, 75 | .obstacle2 { 76 | height: 30px; 77 | bottom: 60px; 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import globals from "globals"; 2 | import pluginJs from "@eslint/js"; 3 | import pluginReactConfig from "eslint-plugin-react/configs/recommended.js"; 4 | import eslintConfigPrettier from "eslint-config-prettier"; 5 | 6 | // mimic CommonJS variables -- not needed if using CommonJS 7 | import { FlatCompat } from "@eslint/eslintrc"; 8 | import path from "path"; 9 | import { fileURLToPath } from "url"; 10 | 11 | // mimic CommonJS variables -- not needed if using CommonJS 12 | const __filename = fileURLToPath(import.meta.url); 13 | const __dirname = path.dirname(__filename); 14 | 15 | const compat = new FlatCompat({ 16 | baseDirectory: __dirname, 17 | }); 18 | 19 | // eslint-disable-next-line import/no-anonymous-default-export 20 | export default [ 21 | { 22 | settings: { 23 | react: { 24 | version: "detect", 25 | }, 26 | }, 27 | }, 28 | { languageOptions: { globals: globals.browser } }, 29 | pluginJs.configs.recommended, 30 | pluginReactConfig, 31 | ...compat.extends("prettier"), 32 | { 33 | rules: { 34 | "no-unused-vars": "error", 35 | "no-undef": "error", 36 | "react/jsx-uses-react": "off", 37 | "react/react-in-jsx-scope": "off", 38 | }, 39 | }, 40 | { 41 | ignores: [ 42 | "cypress", 43 | "node_modules", 44 | "public", 45 | "src/assets", 46 | ".gitignore", 47 | ".prettierrc", 48 | ".prettierignore", 49 | "cypress.config.js", 50 | "eslint.config.mjs", 51 | ], 52 | }, 53 | eslintConfigPrettier, 54 | ]; 55 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mario-jump", 3 | "version": "0.1.0", 4 | "homepage": "https://helloukey.github.io/mario-jump", 5 | "private": true, 6 | "dependencies": { 7 | "@reduxjs/toolkit": "^1.8.5", 8 | "@testing-library/jest-dom": "^5.16.5", 9 | "@testing-library/react": "^13.4.0", 10 | "@testing-library/user-event": "^13.5.0", 11 | "lint-staged": "^15.2.2", 12 | "react": "^18.2.0", 13 | "react-dom": "^18.2.0", 14 | "react-redux": "^8.0.2", 15 | "react-scripts": "5.0.1", 16 | "web-vitals": "^2.1.4" 17 | }, 18 | "lint-staged": { 19 | "**/*": "prettier --write --ignore-unknown", 20 | "*.js": "eslint --cache --fix --max-warnings=0", 21 | "*.--write": "prettier --write" 22 | }, 23 | "scripts": { 24 | "predeploy": "npm run build", 25 | "deploy": "gh-pages -d build", 26 | "start": "react-scripts start", 27 | "build": "react-scripts build", 28 | "test": "react-scripts test", 29 | "eject": "react-scripts eject", 30 | "format": "npx prettier . --write", 31 | "prepare": "husky" 32 | }, 33 | "eslintConfig": { 34 | "extends": [ 35 | "react-app", 36 | "react-app/jest" 37 | ] 38 | }, 39 | "browserslist": { 40 | "production": [ 41 | ">0.2%", 42 | "not dead", 43 | "not op_mini all" 44 | ], 45 | "development": [ 46 | "last 1 chrome version", 47 | "last 1 firefox version", 48 | "last 1 safari version" 49 | ] 50 | }, 51 | "devDependencies": { 52 | "@eslint/eslintrc": "^3.0.2", 53 | "@eslint/js": "^9.0.0", 54 | "cypress": "^13.7.2", 55 | "eslint": "^8.57.0", 56 | "eslint-config-prettier": "^9.1.0", 57 | "eslint-plugin-react": "^7.34.1", 58 | "gh-pages": "^4.0.0", 59 | "globals": "^15.0.0", 60 | "husky": "^9.0.11", 61 | "prettier": "3.2.5" 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | <!doctype html> 2 | <html lang="en"> 3 | <head> 4 | <meta charset="utf-8" /> 5 | <link rel="icon" href="%PUBLIC_URL%/favicon.ico" /> 6 | <meta name="viewport" content="width=device-width, initial-scale=1" /> 7 | <meta name="theme-color" content="#000000" /> 8 | <meta name="description" content="T-Rex Dinosaur game with Mario touch!" /> 9 | <link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" /> 10 | <!-- 11 | manifest.json provides metadata used when your web app is installed on a 12 | user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/ 13 | --> 14 | <link rel="manifest" href="%PUBLIC_URL%/manifest.json" /> 15 | <!-- 16 | Notice the use of %PUBLIC_URL% in the tags above. 17 | It will be replaced with the URL of the `public` folder during the build. 18 | Only files inside the `public` folder can be referenced from the HTML. 19 | 20 | Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will 21 | work correctly both with client-side routing and a non-root public URL. 22 | Learn how to configure a non-root public URL by running `npm run build`. 23 | --> 24 | <title>Mario Jump 25 | 26 | 27 | 28 |
29 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /.github/workflows/deployment.yml: -------------------------------------------------------------------------------- 1 | name: Test and Deploy 2 | on: [push, pull_request] 3 | jobs: 4 | test: 5 | runs-on: ubuntu-latest 6 | steps: 7 | - name: Get Code 8 | uses: actions/checkout@v4 9 | - name: Setup NodeJS 10 | uses: actions/setup-node@v4 11 | with: 12 | node-version: 20 13 | - name: Cypress Run 14 | uses: cypress-io/github-action@v6 15 | with: 16 | start: npm start 17 | build: 18 | needs: test 19 | runs-on: ubuntu-latest 20 | steps: 21 | - name: Get Code 22 | uses: actions/checkout@v4 23 | - name: Setup NodeJS 24 | uses: actions/setup-node@v4 25 | with: 26 | node-version: 20 27 | - name: Install Dependencies 28 | run: npm ci 29 | - name: Build Project 30 | run: npm run build 31 | - name: Upload Build 32 | uses: actions/upload-pages-artifact@v3 33 | with: 34 | path: build 35 | deploy: 36 | needs: build 37 | # Only deploy for the main branch 38 | if: github.ref == 'refs/heads/main' 39 | permissions: 40 | pages: write 41 | id-token: write 42 | environment: 43 | name: github-pages 44 | url: ${{ steps.deployment.outputs.page_url }} 45 | runs-on: ubuntu-latest 46 | steps: 47 | - name: Get Code 48 | uses: actions/checkout@v4 49 | - name: Setup NodeJS 50 | uses: actions/setup-node@v4 51 | with: 52 | node-version: 20 53 | - name: Deploy to GitHub Pages 54 | id: deployment 55 | uses: actions/deploy-pages@v4 56 | -------------------------------------------------------------------------------- /cypress/e2e/home/loading.cy.js: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | describe("Check contents of the loading screen", () => { 4 | // Visit the landing page 5 | beforeEach(() => { 6 | cy.visit("/"); 7 | }) 8 | 9 | // Check for the mario image 10 | it("Check for the mario image", () => { 11 | cy.getCypress("loading-mario").should("exist"); 12 | cy.getCypress("loading-mario").should("be.visible"); 13 | }); 14 | 15 | // Check for the loading text 16 | it("Check for the loading text", () => { 17 | cy.getCypress("loading-loading-text").should("exist"); 18 | cy.getCypress("loading-loading-text").should("be.visible"); 19 | cy.getCypress("loading-loading-text").should("contain.text", "Loading..."); 20 | }); 21 | 22 | // Check for the footer div 23 | it("Check for the footer div", () => { 24 | cy.getCypress("footer-copyright-div").should("exist"); 25 | cy.getCypress("footer-copyright-div").should("be.visible"); 26 | cy.getCypress("footer-copyright-div").should("contain.text", "Copyright ©"); 27 | }); 28 | 29 | // Check for the footer link 30 | it("Check for the footer link", () => { 31 | cy.getCypress("footer-copyright-link").should("exist"); 32 | cy.getCypress("footer-copyright-link").should("be.visible"); 33 | cy.getCypress("footer-copyright-link").should("contain.text", "Kunal Ukey"); 34 | cy.getCypress("footer-copyright-link").should("have.attr", "href", "https://github.com/helloukey"); 35 | }); 36 | 37 | // Check for the enter button 38 | it("Check for the enter button", () => { 39 | cy.wait(6000); 40 | cy.getCypress("loading-enter-button").should("exist"); 41 | cy.getCypress("loading-enter-button").should("be.visible"); 42 | cy.getCypress("loading-enter-button").should("contain.text", "ENTER"); 43 | cy.getCypress("loading-enter-button").click(); 44 | }); 45 | }) 46 | -------------------------------------------------------------------------------- /src/components/MobileControls.js: -------------------------------------------------------------------------------- 1 | import "./MobileControls.css"; 2 | import { useSelector, useDispatch } from "react-redux"; 3 | import { setReady } from "../redux/engineSlice"; 4 | import { marioJumping } from "../redux/marioSlice"; 5 | import { useMemo } from "react"; 6 | import jumpAudio from "../assets/audio/mario-jump.mp3"; 7 | 8 | const MobileControls = () => { 9 | const isPlay = useSelector((state) => state.engine.play); 10 | const mario_jump = useSelector((state) => state.mario.jumping); 11 | const isDie = useSelector((state) => state.engine.die); 12 | const dispatch = useDispatch(); 13 | 14 | const jump = useMemo(() => { 15 | return new Audio(jumpAudio); 16 | }, []); 17 | 18 | const handleStart = () => { 19 | if (!isPlay && !isDie) { 20 | dispatch(setReady(true)); 21 | } 22 | }; 23 | const handleJump = () => { 24 | if (mario_jump === false) { 25 | dispatch(marioJumping(true)); 26 | jump.play(); 27 | setTimeout(() => { 28 | dispatch(marioJumping(false)); 29 | jump.pause(); 30 | jump.currentTime = 0; 31 | }, 400); 32 | } 33 | }; 34 | return ( 35 |
36 | {!isPlay && !isDie && ( 37 | 44 | )} 45 | {isDie && !isPlay && ( 46 | 49 | )} 50 | {isPlay && !isDie && ( 51 | 58 | )} 59 |
60 | ); 61 | }; 62 | export default MobileControls; 63 | -------------------------------------------------------------------------------- /src/redux/obstacleSlice.js: -------------------------------------------------------------------------------- 1 | import { createSlice } from "@reduxjs/toolkit"; 2 | 3 | const initialState = { 4 | obs1Bottom: null, 5 | obs1Height: null, 6 | obs1Left: null, 7 | obs1Right: null, 8 | obs1Top: null, 9 | obs1Width: null, 10 | obs1X: null, 11 | obs1Y: null, 12 | 13 | obs2Bottom: null, 14 | obs2Height: null, 15 | obs2Left: null, 16 | obs2Right: null, 17 | obs2Top: null, 18 | obs2Width: null, 19 | obs2X: null, 20 | obs2Y: null, 21 | }; 22 | 23 | export const obstacleSlice = createSlice({ 24 | name: "obstacle", 25 | initialState, 26 | reducers: { 27 | obstacle1Bottom: (state, action) => { 28 | state.obs1Bottom = action.payload; 29 | }, 30 | obstacle1Height: (state, action) => { 31 | state.obs1Height = action.payload; 32 | }, 33 | obstacle1Left: (state, action) => { 34 | state.obs1Left = action.payload; 35 | }, 36 | obstacle1Right: (state, action) => { 37 | state.obs1Right = action.payload; 38 | }, 39 | obstacle1Top: (state, action) => { 40 | state.obs1Top = action.payload; 41 | }, 42 | obstacle1Width: (state, action) => { 43 | state.obs1Width = action.payload; 44 | }, 45 | obstacle1X: (state, action) => { 46 | state.obs1X = action.payload; 47 | }, 48 | obstacle1Y: (state, action) => { 49 | state.obs1Y = action.payload; 50 | }, 51 | 52 | obstacle2Bottom: (state, action) => { 53 | state.obs2Bottom = action.payload; 54 | }, 55 | obstacle2Height: (state, action) => { 56 | state.obs2Height = action.payload; 57 | }, 58 | obstacle2Left: (state, action) => { 59 | state.obs2Left = action.payload; 60 | }, 61 | obstacle2Right: (state, action) => { 62 | state.obs2Right = action.payload; 63 | }, 64 | obstacle2Top: (state, action) => { 65 | state.obs2Top = action.payload; 66 | }, 67 | obstacle2Width: (state, action) => { 68 | state.obs2Width = action.payload; 69 | }, 70 | obstacle2X: (state, action) => { 71 | state.obs2X = action.payload; 72 | }, 73 | obstacle2Y: (state, action) => { 74 | state.obs2Y = action.payload; 75 | }, 76 | }, 77 | }); 78 | 79 | export const { 80 | obstacle1Bottom, 81 | obstacle1Height, 82 | obstacle1Left, 83 | obstacle1Right, 84 | obstacle1Top, 85 | obstacle1Width, 86 | obstacle1X, 87 | obstacle1Y, 88 | obstacle2Bottom, 89 | obstacle2Height, 90 | obstacle2Left, 91 | obstacle2Right, 92 | obstacle2Top, 93 | obstacle2Width, 94 | obstacle2X, 95 | obstacle2Y, 96 | } = obstacleSlice.actions; 97 | export default obstacleSlice.reducer; 98 | -------------------------------------------------------------------------------- /src/components/Obstacles.js: -------------------------------------------------------------------------------- 1 | import "./Obstacles.css"; 2 | import obstacle1 from "../assets/goombla.gif"; 3 | import obstacle2 from "../assets/koopa.gif"; 4 | import { useRef, useEffect } from "react"; 5 | 6 | // redux 7 | import { useDispatch, useSelector } from "react-redux"; 8 | import { 9 | obstacle1Height, 10 | obstacle1Left, 11 | obstacle1Top, 12 | obstacle1Width, 13 | obstacle2Height, 14 | obstacle2Left, 15 | obstacle2Top, 16 | obstacle2Width, 17 | } from "../redux/obstacleSlice"; 18 | import { setSpeed } from "../redux/engineSlice"; 19 | 20 | const Obstacles = () => { 21 | const dispatch = useDispatch(); 22 | const isPlay = useSelector((state) => state.engine.play); 23 | const speed = useSelector((state) => state.engine.speed); 24 | const obstacle1Ref = useRef(); 25 | const obstacle2Ref = useRef(); 26 | 27 | useEffect(() => { 28 | setInterval(() => { 29 | dispatch( 30 | obstacle1Height(obstacle1Ref.current.getBoundingClientRect().height) 31 | ); 32 | dispatch( 33 | obstacle1Left(obstacle1Ref.current.getBoundingClientRect().left) 34 | ); 35 | dispatch(obstacle1Top(obstacle1Ref.current.getBoundingClientRect().top)); 36 | dispatch( 37 | obstacle1Width(obstacle1Ref.current.getBoundingClientRect().width) 38 | ); 39 | 40 | dispatch( 41 | obstacle2Height(obstacle2Ref.current.getBoundingClientRect().height) 42 | ); 43 | dispatch( 44 | obstacle2Left(obstacle2Ref.current.getBoundingClientRect().left) 45 | ); 46 | dispatch(obstacle2Top(obstacle2Ref.current.getBoundingClientRect().top)); 47 | dispatch( 48 | obstacle2Width(obstacle2Ref.current.getBoundingClientRect().width) 49 | ); 50 | }, 100); 51 | }, [dispatch]); 52 | 53 | useEffect(() => { 54 | if (speed >= 0) { 55 | setTimeout(() => { 56 | dispatch(setSpeed(0.0001)); 57 | }, 1000); 58 | } 59 | }, [speed, dispatch]); 60 | 61 | return ( 62 |
63 | 75 | 87 |
88 | ); 89 | }; 90 | export default Obstacles; 91 | -------------------------------------------------------------------------------- /cypress/e2e/gameplay/play.cy.js: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | describe("Check the gameplay", () => { 4 | // Visit gameplay before each test 5 | beforeEach(() => { 6 | cy.visit("/"); 7 | cy.wait(6000); 8 | cy.getCypress("loading-enter-button").click(); 9 | }); 10 | 11 | // Check for contents & gameplay 12 | it("Check for contents & gameplay", () => { 13 | // Mario image 14 | cy.getCypress("title-mario-logo").should("exist"); 15 | cy.getCypress("title-mario-logo").should("be.visible"); 16 | 17 | // Title 18 | cy.getCypress("title-mario-jump").should("exist"); 19 | cy.getCypress("title-mario-jump").should("be.visible"); 20 | cy.getCypress("title-mario-jump").should("contain.text", "Mario Jump"); 21 | 22 | // Check for the footer div 23 | cy.getCypress("footer-copyright-div").should("exist"); 24 | cy.getCypress("footer-copyright-div").should("be.visible"); 25 | cy.getCypress("footer-copyright-div").should("contain.text", "Copyright ©"); 26 | 27 | // Check for the footer link 28 | cy.getCypress("footer-copyright-link").should("exist"); 29 | cy.getCypress("footer-copyright-link").should("be.visible"); 30 | cy.getCypress("footer-copyright-link").should("contain.text", "Kunal Ukey"); 31 | cy.getCypress("footer-copyright-link").should("have.attr", "href", "https://github.com/helloukey"); 32 | 33 | // Check for score text 34 | cy.getCypress("last-score-text").should("exist"); 35 | cy.getCypress("last-score-text").should("be.visible"); 36 | cy.getCypress("last-score-text").should("contain.text", "Score: 0"); 37 | 38 | // Sun image 39 | cy.getCypress("sun").should("exist"); 40 | cy.getCypress("sun").should("be.visible"); 41 | 42 | // Clouds image 43 | cy.getCypress("clouds").should("exist"); 44 | cy.getCypress("clouds").should("be.visible"); 45 | 46 | // Birds image 47 | cy.getCypress("birds").should("exist"); 48 | cy.getCypress("birds").should("be.visible"); 49 | 50 | // Check for key message text 51 | cy.getCypress("press-title").should("exist"); 52 | cy.getCypress("press-title").should("be.visible"); 53 | cy.getCypress("press-title").should("contain.text", "ENTER KEY - START GAME"); 54 | cy.getCypress("press-subtitle").should("exist"); 55 | cy.getCypress("press-subtitle").should("be.visible"); 56 | cy.getCypress("press-subtitle").should("contain.text", "SPACE KEY - JUMP!"); 57 | 58 | // Mario running 59 | cy.getCypress("mario-running").should("exist"); 60 | cy.getCypress("mario-running").should("be.visible"); 61 | 62 | // Goombla 63 | cy.getCypress("goombla").should("exist"); 64 | cy.getCypress("goombla").should("not.be.visible"); 65 | 66 | // Koopa 67 | cy.getCypress("koopa").should("exist"); 68 | cy.getCypress("koopa").should("not.be.visible"); 69 | 70 | // Brick 71 | cy.getCypress("brick").should("exist"); 72 | cy.getCypress("brick").should("be.visible"); 73 | 74 | // Start button 75 | cy.getCypress("start-button").should("exist"); 76 | cy.getCypress("start-button").should("be.visible"); 77 | cy.getCypress("start-button").should("contain.text", "START"); 78 | cy.getCypress("start-button").click(); 79 | 80 | // Jump button 81 | cy.getCypress("jump-button").should("exist"); 82 | cy.getCypress("jump-button").should("be.visible"); 83 | cy.getCypress("jump-button").should("contain.text", "JUMP"); 84 | 85 | // Game over button 86 | cy.wait(2000); 87 | cy.getCypress("game-over-button").should("exist"); 88 | cy.getCypress("game-over-button").should("be.visible"); 89 | cy.getCypress("game-over-button").should("contain.text", "GAME OVER"); 90 | 91 | // Start button 92 | cy.wait(3000); 93 | cy.getCypress("start-button").should("contain.text", "START"); 94 | 95 | // Score is not 0 96 | cy.getCypress("last-score-text").should("not.contain.text", "Score: 0"); 97 | 98 | }) 99 | }) 100 | -------------------------------------------------------------------------------- /src/components/Mario.js: -------------------------------------------------------------------------------- 1 | import "./Mario.css"; 2 | import MarioCharacter from "../assets/mario-run.gif"; 3 | import { useEffect, useRef, useCallback, useMemo } from "react"; 4 | import jumpAudio from "../assets/audio/mario-jump.mp3"; 5 | import backgroundMusic from "../assets/audio/running-about.mp3"; 6 | // redux 7 | import { useDispatch, useSelector } from "react-redux"; 8 | import { 9 | marioJumping, 10 | marioHeight, 11 | marioLeft, 12 | marioTop, 13 | marioWidth, 14 | } from "../redux/marioSlice"; 15 | import { setReady, setDie, setScore } from "../redux/engineSlice"; 16 | 17 | // die 18 | import dieAudio from "../assets/audio/mario-died.mp3"; 19 | 20 | const Mario = () => { 21 | const marioRef = useRef(); 22 | const dispatch = useDispatch(); 23 | const die = useSelector((state) => state.engine.die); 24 | const loadingScreen = useSelector((state) => state.engine.loadingScreen); 25 | 26 | const isPlay = useSelector((state) => state.engine.play); 27 | // Mario positions & jump 28 | const mario_jump = useSelector((state) => state.mario.jumping); 29 | const mario_height = useSelector((state) => state.mario.height); 30 | const mario_left = useSelector((state) => state.mario.left); 31 | const mario_top = useSelector((state) => state.mario.top); 32 | const mario_width = useSelector((state) => state.mario.width); 33 | // Obstacle1 positions 34 | const obs1_height = useSelector((state) => state.obstacle.obs1Height); 35 | const obs1_left = useSelector((state) => state.obstacle.obs1Left); 36 | const obs1_top = useSelector((state) => state.obstacle.obs1Top); 37 | const obs1_width = useSelector((state) => state.obstacle.obs1Width); 38 | // Obstacle2 positions 39 | const obs2_height = useSelector((state) => state.obstacle.obs2Height); 40 | const obs2_left = useSelector((state) => state.obstacle.obs2Left); 41 | const obs2_top = useSelector((state) => state.obstacle.obs2Top); 42 | const obs2_width = useSelector((state) => state.obstacle.obs2Width); 43 | 44 | // Jump audio 45 | const jump = useMemo(() => { 46 | return new Audio(jumpAudio); 47 | }, []); 48 | 49 | // Die 50 | const marioDie = useMemo(() => { 51 | return new Audio(dieAudio); 52 | }, []); 53 | 54 | const bgMusic = useMemo(() => { 55 | return new Audio(backgroundMusic); 56 | }, []); 57 | 58 | // Handling key press event. 59 | const handleKey = useCallback( 60 | (e) => { 61 | if (e.code === "Enter" && !isPlay && !die && !loadingScreen) { 62 | dispatch(setReady(true)); 63 | } 64 | if ( 65 | mario_jump === false && 66 | e.code === "Space" && 67 | isPlay && 68 | !die && 69 | !loadingScreen 70 | ) { 71 | dispatch(marioJumping(true)); 72 | jump.play(); 73 | setTimeout(() => { 74 | dispatch(marioJumping(false)); 75 | jump.pause(); 76 | jump.currentTime = 0; 77 | }, 400); 78 | } 79 | }, 80 | [mario_jump, jump, dispatch, isPlay, die, loadingScreen] 81 | ); 82 | 83 | useEffect(() => { 84 | if ( 85 | mario_left < obs1_left + obs1_width && 86 | mario_left + mario_width > obs1_left && 87 | mario_top < obs1_top + obs1_height && 88 | mario_top + mario_height > obs1_top 89 | ) { 90 | dispatch(setDie(true)); 91 | marioDie.play(); 92 | dispatch(setReady(false)); 93 | setTimeout(() => { 94 | dispatch(setDie(false)); 95 | }, 2000); 96 | setTimeout(() => { 97 | dispatch(setScore(0)); 98 | }, 100); 99 | } 100 | 101 | if ( 102 | mario_left < obs2_left + obs2_width && 103 | mario_left + mario_width > obs2_left && 104 | mario_top < obs2_top + obs2_height && 105 | mario_top + mario_height > obs2_top 106 | ) { 107 | dispatch(setDie(true)); 108 | marioDie.play(); 109 | dispatch(setReady(false)); 110 | setTimeout(() => { 111 | dispatch(setDie(false)); 112 | }, 2000); 113 | setTimeout(() => { 114 | dispatch(setScore(0)); 115 | }, 100); 116 | } 117 | }, [ 118 | mario_left, 119 | obs1_left, 120 | obs1_width, 121 | mario_width, 122 | mario_top, 123 | obs1_top, 124 | obs1_height, 125 | mario_height, 126 | dispatch, 127 | marioDie, 128 | obs2_left, 129 | obs2_width, 130 | obs2_top, 131 | obs2_height, 132 | ]); 133 | 134 | // Monitor key press. 135 | useEffect(() => { 136 | document.addEventListener("keydown", handleKey); 137 | dispatch(marioHeight(marioRef.current.getBoundingClientRect().height)); 138 | dispatch(marioLeft(marioRef.current.getBoundingClientRect().left)); 139 | dispatch(marioTop(marioRef.current.getBoundingClientRect().top)); 140 | dispatch(marioWidth(marioRef.current.getBoundingClientRect().width)); 141 | 142 | if (isPlay) { 143 | bgMusic.play(); 144 | } else { 145 | bgMusic.pause(); 146 | bgMusic.currentTime = 0; 147 | } 148 | }, [handleKey, dispatch, bgMusic, isPlay]); 149 | 150 | return ( 151 |
152 | {!die && ( 153 | 160 | )} 161 | {die && ( 162 | 168 | )} 169 |
170 | ); 171 | }; 172 | export default Mario; 173 | -------------------------------------------------------------------------------- /src/assets/brick.svg: -------------------------------------------------------------------------------- 1 | --------------------------------------------------------------------------------