├── .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 |
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 |
15 |
16 |
17 | );
18 |
--------------------------------------------------------------------------------
/src/components/Footer.js:
--------------------------------------------------------------------------------
1 | import "./Footer.css";
2 |
3 | const Footer = () => {
4 | return (
5 |
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 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 | 
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 |
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 |
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 |
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 && }
23 |
24 | {!isPlay && }
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
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 |
25 | {play && (
26 |
27 | Score: {score}
28 |
29 | )}
30 | {!play && (
31 |
32 | Score: {lastScore}
33 |
34 | )}
35 |
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 |
19 |

25 | {!isReady && (
26 |
27 | Loading...
28 |
29 | )}
30 | {isReady && (
31 |
38 | )}
39 |
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 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
14 |
15 |
24 | 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 |
--------------------------------------------------------------------------------