diff --git a/client/package-lock.json b/client/package-lock.json index b1f81e4..26f0575 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -14,6 +14,7 @@ "formik": "^2.4.5", "framer-motion": "^11.0.3", "leaflet": "^1.9.4", + "phaser": "^3.80.1", "react": "^18.2.0", "react-cookie": "^7.0.2", "react-dom": "^18.2.0", @@ -3081,6 +3082,11 @@ "node": ">=0.10.0" } }, + "node_modules/eventemitter3": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", + "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==" + }, "node_modules/execa": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz", @@ -4753,6 +4759,14 @@ "node": "*" } }, + "node_modules/phaser": { + "version": "3.80.1", + "resolved": "https://registry.npmjs.org/phaser/-/phaser-3.80.1.tgz", + "integrity": "sha512-VQGAWoDOkEpAWYkI+PUADv5Ql+SM0xpLuAMBJHz9tBcOLqjJ2wd8bUhxJgOqclQlLTg97NmMd9MhS75w16x1Cw==", + "dependencies": { + "eventemitter3": "^5.0.1" + } + }, "node_modules/picocolors": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", diff --git a/client/package.json b/client/package.json index 283c356..e762023 100644 --- a/client/package.json +++ b/client/package.json @@ -28,7 +28,8 @@ "react-qr-reader-es6": "^2.2.1-2", "react-router": "^6.21.3", "react-router-dom": "^6.22.1", - "yup": "^1.3.3" + "yup": "^1.3.3", + "phaser": "^3.80.1" }, "devDependencies": { "@testing-library/react": "^14.1.2", diff --git a/client/public/assets/game/javelin/gauge.png b/client/public/assets/game/javelin/gauge.png new file mode 100644 index 0000000..aecb2af Binary files /dev/null and b/client/public/assets/game/javelin/gauge.png differ diff --git a/client/public/assets/game/runner/audio/tap.mp3 b/client/public/assets/game/runner/audio/tap.mp3 new file mode 100644 index 0000000..89efa49 Binary files /dev/null and b/client/public/assets/game/runner/audio/tap.mp3 differ diff --git "a/client/public/assets/img/game/runner/PisteD\303\251part.png" b/client/public/assets/game/runner/bg-start-track.png similarity index 100% rename from "client/public/assets/img/game/runner/PisteD\303\251part.png" rename to client/public/assets/game/runner/bg-start-track.png diff --git a/client/public/assets/img/game/runner/PisteNormale.png b/client/public/assets/game/runner/bg-track.png similarity index 100% rename from client/public/assets/img/game/runner/PisteNormale.png rename to client/public/assets/game/runner/bg-track.png diff --git a/client/public/assets/game/runner/flag-scratched-sprite.png b/client/public/assets/game/runner/flag-scratched-sprite.png new file mode 100644 index 0000000..ca980bb Binary files /dev/null and b/client/public/assets/game/runner/flag-scratched-sprite.png differ diff --git a/client/public/assets/game/runner/flag-scratching-sprite.png b/client/public/assets/game/runner/flag-scratching-sprite.png new file mode 100644 index 0000000..22c8d45 Binary files /dev/null and b/client/public/assets/game/runner/flag-scratching-sprite.png differ diff --git a/client/public/assets/game/runner/flag.png b/client/public/assets/game/runner/flag.png new file mode 100644 index 0000000..8237e0e Binary files /dev/null and b/client/public/assets/game/runner/flag.png differ diff --git a/client/public/assets/game/runner/hidden-runner.png b/client/public/assets/game/runner/hidden-runner.png new file mode 100644 index 0000000..3baa572 Binary files /dev/null and b/client/public/assets/game/runner/hidden-runner.png differ diff --git a/client/public/assets/game/runner/prerun-anim-sprite.png b/client/public/assets/game/runner/prerun-anim-sprite.png new file mode 100644 index 0000000..81f510e Binary files /dev/null and b/client/public/assets/game/runner/prerun-anim-sprite.png differ diff --git a/client/public/assets/game/runner/run-anim-sprite.png b/client/public/assets/game/runner/run-anim-sprite.png new file mode 100644 index 0000000..bd81bcf Binary files /dev/null and b/client/public/assets/game/runner/run-anim-sprite.png differ diff --git a/client/public/assets/img/game/runner/TapSprite.png b/client/public/assets/game/runner/tap.png similarity index 100% rename from client/public/assets/img/game/runner/TapSprite.png rename to client/public/assets/game/runner/tap.png diff --git "a/client/public/assets/img/game/runner/Arriv\303\251eIdle.gif" "b/client/public/assets/img/game/runner/Arriv\303\251eIdle.gif" deleted file mode 100644 index 5e71cfd..0000000 Binary files "a/client/public/assets/img/game/runner/Arriv\303\251eIdle.gif" and /dev/null differ diff --git "a/client/public/assets/img/game/runner/Arriv\303\251eScratch.gif" "b/client/public/assets/img/game/runner/Arriv\303\251eScratch.gif" deleted file mode 100644 index d2bb398..0000000 Binary files "a/client/public/assets/img/game/runner/Arriv\303\251eScratch.gif" and /dev/null differ diff --git a/client/public/assets/img/game/runner/CacheCourse.png b/client/public/assets/img/game/runner/CacheCourse.png deleted file mode 100644 index 1ce22b4..0000000 Binary files a/client/public/assets/img/game/runner/CacheCourse.png and /dev/null differ diff --git a/client/public/assets/img/game/runner/RunAnim.gif b/client/public/assets/img/game/runner/RunAnim.gif deleted file mode 100644 index 327f038..0000000 Binary files a/client/public/assets/img/game/runner/RunAnim.gif and /dev/null differ diff --git a/client/public/assets/img/game/runner/RunPrepAnim.gif b/client/public/assets/img/game/runner/RunPrepAnim.gif deleted file mode 100644 index 8e04bf0..0000000 Binary files a/client/public/assets/img/game/runner/RunPrepAnim.gif and /dev/null differ diff --git a/client/public/assets/img/game/runner/WinAnim.gif b/client/public/assets/img/game/runner/WinAnim.gif deleted file mode 100644 index 2835033..0000000 Binary files a/client/public/assets/img/game/runner/WinAnim.gif and /dev/null differ diff --git a/client/src/components/game/EventBus.ts b/client/src/components/game/EventBus.ts new file mode 100644 index 0000000..9e4ebbf --- /dev/null +++ b/client/src/components/game/EventBus.ts @@ -0,0 +1,3 @@ +import Phaser from "phaser"; + +export const EventBus = new Phaser.Events.EventEmitter(); diff --git a/client/src/components/game/JavelinThrowGame.tsx b/client/src/components/game/JavelinThrowGame.tsx new file mode 100644 index 0000000..e679ac1 --- /dev/null +++ b/client/src/components/game/JavelinThrowGame.tsx @@ -0,0 +1,21 @@ +import { useEffect, useRef } from "react"; +import StartGame from "./main"; +// import { EventBus } from "./EventBus"; + +export interface IRefRunnerGame { + game: Phaser.Game | null; +} + +const JavelinThrowGame = () => { + const gameRef = useRef(null); + + useEffect(() => { + if (gameRef.current === null) { + gameRef.current = StartGame("game-container"); + } + }); + + return
; +}; + +export default JavelinThrowGame; diff --git a/client/src/components/game/RunnerGame.tsx b/client/src/components/game/RunnerGame.tsx index 465f8d2..4eb5141 100644 --- a/client/src/components/game/RunnerGame.tsx +++ b/client/src/components/game/RunnerGame.tsx @@ -1,147 +1,36 @@ -import { motion } from "framer-motion"; -import { useState, useEffect } from "react"; -import { APIHandler } from "../../utils/api/api-handler"; -import { useAuth } from "../../hooks/useAuth"; +import React, { useEffect, useRef } from "react"; +import StartGame from "./main"; +import { EventBus } from "./EventBus"; -interface Click { - x: number; - y: number; +export interface IRefRunnerGame { + game: Phaser.Game | null; + timer: number | null; } -const RunnerGame = () => { - const { token } = useAuth(); - const [clicks, setClicks] = useState([]); - const [isStarting, setIsStarting] = useState(false); - const [gameOver, setGameOver] = useState(false); - const [score, setScore] = useState(0); - const maxClicks = 6; +const RunnerGame = ({ + setTime, +}: { + setTime: React.Dispatch>; +}) => { + const gameRef = useRef(null); - const handleClick = (e: React.MouseEvent) => { - setIsStarting(true); - const newClick = { x: e.clientX, y: e.clientY }; - - const newClicks = [...clicks, newClick]; - - if (newClicks.length > maxClicks) { - newClicks.splice(0, newClicks.length - maxClicks); - } - - setClicks(newClicks); - - if (!gameOver) { - setScore((prevScore) => prevScore + 1); + useEffect(() => { + if (gameRef.current === null) { + gameRef.current = StartGame("game-container"); } - setTimeout(() => { - setClicks((prevClicks) => prevClicks.slice(1)); - }, 3000); - }; + const timerListener = (timer: number) => { + setTime(timer); + }; - useEffect(() => { - setTimeout(() => { - setGameOver(true); - APIHandler("/my/flami/training", false, "PATCH", { - worked_stat: "speed" - }, token) - }, 9000); - }, []); + EventBus.on("timer", timerListener); - return ( -
- {isStarting ? ( -
Start
- ) : ( -
Pas start
- )} - {clicks.map((click, index) => ( - - ))} -
-
- - + return () => { + EventBus.removeListener("timer", timerListener); + }; + }); - - - -
- {isStarting && !gameOver ? ( - <> - - - ) : ( - <> - - - )} -
- {gameOver && ( -
- Bravo tu as été rapide ! Score : {score} -
- )} -
- ); + return
; }; export default RunnerGame; diff --git a/client/src/components/game/main.ts b/client/src/components/game/main.ts new file mode 100644 index 0000000..9d43931 --- /dev/null +++ b/client/src/components/game/main.ts @@ -0,0 +1,18 @@ +import Phaser from "phaser"; +import { Runner } from "./scenes/Runner"; +// import { JavelinThrow } from "./scenes/JavelinThrow"; + +const config: Phaser.Types.Core.GameConfig = { + type: Phaser.AUTO, + scale: { + mode: Phaser.Scale.RESIZE, + }, + parent: "game-container", + scene: [Runner], +}; + +const StartGame = (parent: string) => { + return new Phaser.Game({ ...config, parent: parent }); +}; + +export default StartGame; diff --git a/client/src/components/game/scenes/JavelinThrow.ts b/client/src/components/game/scenes/JavelinThrow.ts new file mode 100644 index 0000000..c0a564a --- /dev/null +++ b/client/src/components/game/scenes/JavelinThrow.ts @@ -0,0 +1,98 @@ +export class JavelinThrow extends Phaser.Scene { + private gauge!: Phaser.GameObjects.Image; + private maskGraphics!: Phaser.GameObjects.Graphics; + private maskWidth: number = -250; + private chargeSpeed: number = 2; + private startingGame: boolean = false; + private power: number = 0; + + constructor() { + super("JavelinThrow"); + } + + preload() { + this.load.image("gauge", "assets/javelin/gauge.png"); + } + + create() { + this.gauge = this.add.image(this.cameras.main.width / 2, 100, "gauge"); + this.gauge.displayWidth = 300; + this.gauge.displayHeight = 150; + + this.maskGraphics = this.add.graphics(); + this.maskGraphics.fillStyle(0x00000); + this.maskGraphics.fillRect( + this.cameras.main.width / 2 + 125, + 0, + this.maskWidth, + this.gauge.displayHeight + ); + + this.input.on("pointerdown", () => { + this.startingGame = true; + }); + + this.input.on("pointerup", () => { + this.launchJavelin(); + this.startingGame = false; + }); + } + + update() { + if (this.startingGame) { + this.maskWidth += this.chargeSpeed; + + if (this.maskWidth > 0) { + return; + } + + this.maskGraphics.clear(); + this.maskGraphics.fillRect( + this.cameras.main.width / 2 + 125, + 0, + this.maskWidth, + this.gauge.displayHeight + ); + + this.getPosition(this.maskWidth); + } + } + + getPosition(position: number) { + switch (true) { + case position >= -250 && position < -192: + this.power = 0; + break; + case position >= -192 && position < -143: + this.power = 25; + break; + case position >= -143 && position < -118: + this.power = 50; + break; + case position >= -118 && position < -92: + this.power = 75; + break; + case position >= -92 && position < -62: + this.power = 99; + break; + case position >= -62 && position < -48: + this.power = 75; + break; + case position >= -48 && position < -34: + this.power = 50; + break; + case position >= -34 && position < -18: + this.power = 25; + break; + case position >= -18 && position <= 0: + this.power = 0; + break; + default: + break; + } + } + + launchJavelin() { + console.log("Javelin launched with power:", this.power); + } +} diff --git a/client/src/components/game/scenes/Runner.ts b/client/src/components/game/scenes/Runner.ts new file mode 100644 index 0000000..aa1c59d --- /dev/null +++ b/client/src/components/game/scenes/Runner.ts @@ -0,0 +1,274 @@ +import Phaser from "phaser"; +import { EventBus } from "../EventBus"; + +export class Runner extends Phaser.Scene { + private character!: Phaser.GameObjects.Sprite; + private startTrack!: Phaser.GameObjects.Image; + private flagNormal!: Phaser.GameObjects.Image; + private flagScratched!: Phaser.GameObjects.Sprite; + private normalTracks: Phaser.GameObjects.Image[] = []; + private tapImages: Phaser.GameObjects.Image[] = []; + private timerText!: Phaser.GameObjects.Text; + public elapsedTime: number = 0; + private timerInterval!: number; + private isRunning: boolean = false; + private scrollSpeed: number = 0; + private trackCount: number = 19; + + constructor() { + super("Runner"); + } + + preload() { + this.load.spritesheet( + "prepare", + "/assets/game/runner/prerun-anim-sprite.png", + { + frameWidth: 512, + frameHeight: 512, + } + ); + this.load.spritesheet("run", "/assets/game/runner/run-anim-sprite.png", { + frameWidth: 565, + frameHeight: 623, + }); + + this.load.image("start-track", "/assets/game/runner/bg-start-track.png"); + this.load.image("track", "/assets/game/runner/bg-track.png"); + this.load.image("background", "/assets/game/runner/hidden-runner.png"); + this.load.image("flag", "/assets/game/runner/flag.png"); + + this.load.spritesheet( + "scratched-flag", + "/assets/game/runner/flag-scratched-sprite.png", + { + frameWidth: 512, + frameHeight: 512, + } + ); + + this.load.image("tap", "/assets/game/runner/tap.png"); + this.load.audio("tap-audio", "/assets/game/runner/audio/tap.mp3"); + } + + create() { + this.startTrack = this.add.image( + this.cameras.main.width / 2, + this.cameras.main.height / 1.6, + "start-track" + ); + this.startTrack.displayWidth = 320; + this.startTrack.displayHeight = 200; + + for (let i = 0; i < this.trackCount; i++) { + const track = this.add.image( + this.startTrack.x + this.startTrack.displayWidth * (i + 1), + this.cameras.main.height / 1.6, + "track" + ); + track.displayWidth = 320; + track.displayHeight = 200; + + this.normalTracks.push(track); + } + + this.anims.create({ + key: "scratched", + frames: this.anims.generateFrameNumbers("scratched-flag", { + start: 0, + end: 23, + }), + frameRate: 20, + repeat: -1, + }); + + const lastTrack = this.normalTracks[16]; + this.flagNormal = this.add.sprite( + lastTrack.x + lastTrack.displayWidth / 2 - 150, + lastTrack.y - 35, + "flag" + ); + this.flagNormal.scale = 0.15; + + this.flagNormal.visible = true; + + this.flagScratched = this.add + .sprite( + lastTrack.x + lastTrack.displayWidth / 2 - 150, + lastTrack.y - 35, + "scratched-flag" + ) + .play("scratched"); + this.flagScratched.displayHeight = 300; + this.flagScratched.displayWidth = 350; + + this.flagScratched.visible = false; + + const centerX = this.cameras.main.width / 2; + const centerY = this.cameras.main.height / 2; + const circleRadius = 160; + + const bg = this.add.image(centerX, centerY, "background"); + bg.displayHeight = this.cameras.main.height; + bg.displayWidth = this.cameras.main.width; + + const maskCircle = this.make.graphics(); + maskCircle.fillCircle(centerX, centerY, circleRadius); + + const mask = maskCircle.createGeometryMask(); + + mask.invertAlpha = true; + + bg.setMask(mask); + + this.anims.create({ + key: "staying", + frames: this.anims.generateFrameNumbers("prepare", { + start: 0, + end: 39, + }), + frameRate: 20, + repeat: -1, + }); + + this.anims.create({ + key: "running", + frames: this.anims.generateFrameNumbers("run", { + start: 0, + end: 16, + }), + frameRate: 20, + repeat: -1, + }); + + this.character = this.add + .sprite( + this.cameras.main.width * 0.3, + this.cameras.main.height * 0.56, + "prepare" + ) + .play("staying") + .setScale(0.4); + + const tapSound = this.sound.add("tap-audio"); + + this.input.on("pointerdown", (pointer: Phaser.Input.Pointer) => { + const tapImage = this.add.image(pointer.x, pointer.y, "tap"); + tapImage.displayHeight = 64; + tapImage.displayWidth = 64; + this.tapImages.push(tapImage); + + tapSound.play(); + + if (this.tapImages.length > 4) { + const removedTapImage = this.tapImages.shift(); + removedTapImage?.destroy(); + } + + if (!this.isRunning) { + this.startRace(); + } else { + this.increaseSpeed(); + } + }); + + setInterval(() => { + if (this.tapImages.length > 0) { + const removedTapImage = this.tapImages.shift(); + removedTapImage?.destroy(); + } + }, 500); + + this.timerText = this.add.text(20, 20, "Temps: 00:00", { + fontSize: "24px", + fontFamily: "Arial", + color: "#000", + }); + } + + update() { + this.startTrack.x -= this.scrollSpeed; + for (const track of this.normalTracks) { + track.x -= this.scrollSpeed; + } + + const lastTrack = this.normalTracks[16]; + this.flagNormal.x = lastTrack.x + lastTrack.displayWidth / 2 - 150; + this.flagNormal.y = lastTrack.y - 35; + this.flagScratched.x = lastTrack.x + lastTrack.displayWidth / 2 - 150; + this.flagScratched.y = lastTrack.y - 35; + + if (this.character.x >= this.flagNormal.x && this.isRunning) { + this.flagNormal.visible = false; + this.flagScratched.visible = true; + this.finishRace(); + } + + if (!this.isRunning) { + clearInterval(this.timerInterval); + } + } + + startTimer() { + this.timerInterval = setInterval(() => { + this.elapsedTime += 1; + const minutes = Math.floor(this.elapsedTime / 60); + const seconds = this.elapsedTime % 60; + const formattedTime = `${minutes.toString().padStart(2, "0")}:${seconds + .toString() + .padStart(2, "0")}`; + this.timerText.setText(`Temps: ${formattedTime}`); + + EventBus.emit("timer", this.elapsedTime); + }, 1000); + } + + startRace() { + if (!this.isRunning) { + if (this.character.alpha === 0) { + return; + } + + this.startTimer(); + this.isRunning = true; + this.scrollSpeed = 2; + + this.character.visible = false; + + if (this.isRunning) { + this.character = this.add + .sprite( + this.cameras.main.width * 0.4, + this.cameras.main.height * 0.56, + "run" + ) + .play("running") + .setScale(0.4); + } + } + } + + increaseSpeed() { + const maxSpeed = 16; + + if (this.scrollSpeed < maxSpeed) { + this.scrollSpeed += 0.2; + + if (this.scrollSpeed > maxSpeed) { + this.scrollSpeed = maxSpeed; + } + } + } + + finishRace() { + this.tweens.add({ + targets: this.character, + alpha: 0, + duration: 50, + onComplete: () => { + this.isRunning = false; + this.scrollSpeed = 0; + }, + }); + } +} diff --git a/client/src/pages/activities/TrainingPage.tsx b/client/src/pages/activities/TrainingPage.tsx index b96f68e..a8f0058 100644 --- a/client/src/pages/activities/TrainingPage.tsx +++ b/client/src/pages/activities/TrainingPage.tsx @@ -1,11 +1,11 @@ -// import RunnerGame from "../../components/game/RunnerGame"; +import RunnerGame from "../../components/game/RunnerGame"; import { useTheme } from "../../hooks/useTheme"; const TrainingPage = () => { const { setShowNav } = useTheme(); setShowNav(false); - return <>{/* */}; + return <>{}; }; export default TrainingPage;