diff --git a/front/package.json b/front/package.json index e1132b0..36a46cd 100644 --- a/front/package.json +++ b/front/package.json @@ -10,10 +10,15 @@ "preview": "vite preview" }, "dependencies": { + "@react-three/drei": "^9.120.0", + "@react-three/fiber": "^8.17.10", + "@types/three": "^0.170.0", "react": "^18.3.1", "react-dom": "^18.3.1", "react-router": "^7.0.2", - "react-router-dom": "^6.2.1" + "react-router-dom": "^6.2.1", + "three": "^0.171.0", + "three-stdlib": "^2.34.0" }, "devDependencies": { "@eslint/js": "^9.15.0", diff --git a/front/public/models/man.glb b/front/public/models/man.glb new file mode 100644 index 0000000..abd24f6 Binary files /dev/null and b/front/public/models/man.glb differ diff --git a/front/src/components/3d/Axes.tsx b/front/src/components/3d/Axes.tsx new file mode 100644 index 0000000..195e266 --- /dev/null +++ b/front/src/components/3d/Axes.tsx @@ -0,0 +1,31 @@ +import React from 'react' +import { LineBasicMaterial, Line, Group } from 'three' +import { useFrame } from '@react-three/fiber' +import * as THREE from 'three' + +export default function Axes() { + const axes = new Group() + + const x = new Line( + new THREE.BufferGeometry().setFromPoints([new THREE.Vector3(0, 0, 0), + new THREE.Vector3(100, 0, 0)]), + new LineBasicMaterial({ color: 0xff0000 }) // red + ) + const y = new Line( + new THREE.BufferGeometry().setFromPoints([new THREE.Vector3(0, 0, 0), + new THREE.Vector3(0, 100, 0)]), + new LineBasicMaterial({ color: 0x00ff00 }) // green + ) + const z = new Line( + new THREE.BufferGeometry().setFromPoints([new THREE.Vector3(0, 0, 0), + new THREE.Vector3(0, 0, 100)]), + new LineBasicMaterial({ color: 0x0000ff }) // blue + ) + + axes.add(x) + axes.add(y) + axes.add(z) + + + return +} \ No newline at end of file diff --git a/front/src/components/3d/Character.tsx b/front/src/components/3d/Character.tsx new file mode 100644 index 0000000..f6d7d66 --- /dev/null +++ b/front/src/components/3d/Character.tsx @@ -0,0 +1,25 @@ +import { Group, Mesh, MeshStandardMaterial, BufferGeometry } from 'three' +import { useGLTF } from '@react-three/drei' + +export default function Character() { + // import glb file + + // load the glb file in "/models/BASEmodel.glb" + const { nodes, materials, scene } = useGLTF('/models/man.glb') + + // rotate the character + scene.rotation.x = -Math.PI / 2 + scene.rotation.y = 0 + + // enfoncer le personnage dans le sol + scene.position.y = -1 + + return ( + + + + + ) + + +} \ No newline at end of file diff --git a/front/src/components/3d/Floor.tsx b/front/src/components/3d/Floor.tsx new file mode 100644 index 0000000..dbb054b --- /dev/null +++ b/front/src/components/3d/Floor.tsx @@ -0,0 +1,20 @@ + +import React from 'react' +import { Group } from 'three' +import * as THREE from 'three' + + +export default function Floor() { + const floor = new Group() + + const floorGeometry = new THREE.PlaneGeometry(100, 100, 100, 100) + const floorMaterial = new THREE.MeshStandardMaterial({ color: 0x00ff00, side: THREE.DoubleSide }) + const floorMesh = new THREE.Mesh(floorGeometry, floorMaterial) + + floorMesh.rotation.x = -Math.PI / 2 + floorMesh.position.y = -2 + + floor.add(floorMesh) + + return +} \ No newline at end of file diff --git a/front/src/components/3d/Marker.tsx b/front/src/components/3d/Marker.tsx new file mode 100644 index 0000000..893f4f1 --- /dev/null +++ b/front/src/components/3d/Marker.tsx @@ -0,0 +1,23 @@ +import React from 'react' +import { Group } from 'three' +import * as THREE from 'three' + +interface MarkerProps { + position: [number, number, number], + color: string, + onClick?: () => void +} + +export default function Marker({ position, color, onClick }: MarkerProps) { + + const [positionState, setPositionState] = React.useState(position) + + // Return the marker object + // return + return ( + setPositionState([positionState[0], positionState[1], positionState[2] + 0.1])} onPointerOut={(e) => setPositionState(position)}> + + + + ) +} diff --git a/front/src/components/3d/Ocean.tsx b/front/src/components/3d/Ocean.tsx new file mode 100644 index 0000000..6b9d8cf --- /dev/null +++ b/front/src/components/3d/Ocean.tsx @@ -0,0 +1,53 @@ +// src/components/Ocean.tsx +import React, { useEffect, useRef } from 'react'; +import * as THREE from 'three'; +import { Water, WaterOptions } from 'three/examples/jsm/objects/Water.js'; +import { WaterMesh, WaterMeshOptions } from 'three/examples/jsm/objects/Water2Mesh.js'; + +const Ocean: React.FC = () => { + + const waterGeometry = new THREE.PlaneGeometry(10000, 10000); + const waterOption:WaterOptions = { + textureWidth: 512, + textureHeight: 512, + waterNormals: new THREE.TextureLoader().load('https://threejs.org/examples/textures/waternormals.jpg', function (texture) { + texture.wrapS = texture.wrapT = THREE.RepeatWrapping; + }), + alpha: 0.9, + sunDirection: new THREE.Vector3(), + sunColor: 0xffffff, + waterColor: '#001e0f', + distortionScale: 3.7, + fog: true, + + + }; + const water = new Water(waterGeometry, waterOption); + water.rotation.x = -Math.PI / 2; + water.position.y = -1; + + + const waterRef = useRef(null); + useEffect(() => { + if (waterRef.current) { + waterRef.current.add(water); + } + }, [waterRef]); + + + useEffect(() => { + const animate = () => { + requestAnimationFrame(animate); + water.material.uniforms['time'].value += 0.1 / 60.0; + }; + animate(); + }, []); + + return ( + + + + ); +}; + +export default Ocean; diff --git a/front/src/components/Modal.tsx b/front/src/components/Modal.tsx new file mode 100644 index 0000000..083699f --- /dev/null +++ b/front/src/components/Modal.tsx @@ -0,0 +1,78 @@ +import React, { useState } from "react"; + +interface ModalProps { + state: "none" | "flex", + setState: (state: "none" | "flex") => void + question : { + question: string, + reponse_idx: number, + choix: string[] + }, + valides?: [boolean[], React.Dispatch>], + markerIndex?: number +} + +export default function Modal({ state, setState, question, valides, markerIndex }: ModalProps) { + const [isSuccess, setIsSuccess] = useState(false); + const [isSubmitted, setIsSubmitted] = useState(false); + console.log(state, setState, question) + return ( +
+
+ { + isSuccess ?

CONGRATULATIONS

: isSubmitted && !isSuccess &&

WRONG : CORRECT ANSWER IS {question.choix[question.reponse_idx]}

+ } +

{question.question}

+
+ { + question.choix.map((choix, index) => ( + + )) + } +
+ +
+
+
+ + )} \ No newline at end of file diff --git a/front/src/pages/GamePage.tsx b/front/src/pages/GamePage.tsx index f984288..69552df 100644 --- a/front/src/pages/GamePage.tsx +++ b/front/src/pages/GamePage.tsx @@ -1,9 +1,154 @@ +import { Canvas } from "@react-three/fiber"; +import { OrbitControls, PerspectiveCamera, Sky } from "@react-three/drei"; +import * as THREE from "three"; +import React, { useEffect, useRef, useState } from "react"; +import Ocean from "../components/3d/Ocean"; +import Axes from "../components/3d/Axes"; +import Character from "../components/3d/Character"; +import Floor from "../components/3d/Floor"; +import Marker from "../components/3d/Marker"; +import Modal from "../components/Modal"; +import { randInt } from "three/src/math/MathUtils.js"; + + + + + + + + + + export default function GamePage() { + const [stateModal, setStateModal] = useState("none" as "none" | "flex"); + const [questionIndex, setQuestionIndex] = useState(0); + + const QuestionList = [ + // Ensemble de questions sur la polution des océans + // {question:string, reponse_idx:int, choix:[string, string, string, string]} + { + question: "Quel est le plus grand océan du monde ?", + reponse_idx: 1, + choix: ["Océan Atlantique", "Océan Pacifique", "Océan Indien", "Océan Arctique"] + }, + { + question: "Comment s'appelle les continents de plastique dans les océans ?", + reponse_idx: 2, + choix: ["Plastico", "Plastiland", "Plastiques", "Plastiques"] + }, + { + question: "Quel est le nom de la grande masse d'eau salée qui recouvre la majorité de la surface de la Terre ?", + reponse_idx: 3, + choix: ["Mer", "Lac", "Océan", "Rivière"] + }, + { + question: "Quel est l'océan le plus froid ?", + reponse_idx: 0, + choix: ["Océan Arctique", "Océan Antarctique", "Océan Pacifique", "Océan Atlantique"] + }, + { + question: "Quel est l'océan le plus chaud ?", + reponse_idx: 1, + choix: ["Océan Arctique", "Océan Antarctique", "Océan Pacifique", "Océan Atlantique"] + }, + { + question: "Quel est l'océan le plus profond ?", + reponse_idx: 1, + choix: ["Océan Arctique", "Océan Antarctique", "Océan Pacifique", "Océan Atlantique"] + }, + { + question: "Quel est l'océan le plus grand ?", + reponse_idx: 2, + choix: ["Océan Arctique", "Océan Antarctique", "Océan Pacifique", "Océan Atlantique"] + } + ] + + + const [valides, setValides] = useState([false, false, false, false, false, false]); + const [markerIndex, setMarkerIndex] = useState(0); + + const askQuestion = (markerIndex: number) => { + setQuestionIndex(randInt(0, QuestionList.length - 1)); + setStateModal("flex"); + setMarkerIndex(markerIndex); + } + + const MakerList = [ + { position: [0, -0.1, 0], onClick: () => askQuestion(0) }, // body + { position: [-0.5, -0.1, 2.5], onClick: () => askQuestion(1) }, // left foot + { position: [0.5, -0.1, 2.5], onClick: () => askQuestion(2) }, // right foot + { position: [-1, -0.1, 1], onClick: () => askQuestion(3) }, // left hand + { position: [1, -0.1, 1], onClick: () => askQuestion(4) }, // right hand + { position: [0, -0.1, -1], onClick: () => askQuestion(5) }, // head + ] + + // Win condition + useEffect(() => { + if (valides.every(valide => valide)) { + + } + }, [valides]) + + + console.log(stateModal, setStateModal); return ( -
-

Game Page

+
+ { !valides.every(valide => valide) === true ? +

Hint : Répondez correctement aux questions pour sauver le personnage

+ :

Good Game : Bravo, vous avez sauvé le personnage

+ } + + + + + + + + {/* */} + {MakerList.map((marker, index) => ( + + ))} + + +
) -} - +} \ No newline at end of file