diff --git a/index.html b/index.html new file mode 100644 index 0000000000000000000000000000000000000000..1a1b90b6ba97ae029c4a3326ed27c300e82ee4aa --- /dev/null +++ b/index.html @@ -0,0 +1,76 @@ +<!DOCTYPE html> +<html lang="fr"> + <head> + <meta charset="UTF-8" /> + <title>Barbapapa Battle Royale - Multijoueur</title> + <style> + body { + margin: 0; + overflow: hidden; + font-family: sans-serif; + } + /* Menu de démarrage centré */ + #startMenu { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + background: rgba(0, 0, 0, 0.8); + color: white; + padding: 20px; + border-radius: 10px; + text-align: center; + z-index: 10; + } + #startMenu h1 { + margin-top: 0; + } + #startMenu p { + margin: 10px 0; + } + #startMenu input, #startMenu button { + margin: 5px; + padding: 8px; + font-size: 16px; + } + /* Timer en haut à droite */ + #timer { + position: absolute; + top: 10px; + right: 10px; + font-size: 24px; + color: white; + z-index: 10; + display: none; + } + </style> + </head> + <body> + <!-- Menu de démarrage --> + <div id="startMenu"> + <h1>Barbapapa Battle Royale</h1> + <div id="gameOptions"> + <button id="createGameButton">Créer une partie</button> + <br /> + <input type="text" id="roomIdInput" placeholder="ID de la partie" /> + <button id="joinGameButton">Rejoindre une partie</button> + </div> + <div id="playerSettings"> + <p>Utilisez le clavier (W, A, S, D) pour vous déplacer.</p> + <p>Utilisez la souris pour viser et cliquez pour tirer.</p> + </div> + <div id="shareLink" style="display: none;"> + <p>Partagez ce lien pour inviter d'autres joueurs :</p> + <input type="text" id="shareLinkInput" readonly /> + </div> + </div> + + <!-- Timer de la partie --> + <div id="timer">Temps: 00:00</div> + <!-- Barre de vie --> + <div id="healthDisplay" style="position:absolute; top:10px; left:10px; font-size:24px; color:red; z-index:10;"></div> + + <!-- Le script principal (par exemple, bundlé avec Vite ou Webpack) --> + <script type="module" src="/src/index.js"></script> + </body> +</html> diff --git a/package.json b/package.json index 79a2f4c4a411ae1f3f7f17aa78585ab7ed9fabd9..d3b34a72a0d41c8448ebb2eaeca73dcffbae4be8 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,27 @@ { "dependencies": { + "express": "^4.21.2", + "socket.io": "^4.8.1", + "socket.io-client": "^4.8.1", + "three": "^0.174.0", "three.js": "^0.77.1" - } + }, + "name": "barbapapa-br", + "version": "1.0.0", + "main": "index.js", + "devDependencies": { + "vite": "^6.2.1" + }, + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1", + "dev": "vite" + }, + "repository": { + "type": "git", + "url": "ssh://git@ssh.hesge.ch:10572/isc3/pratique_metier/barbapapa-br.git" + }, + "keywords": [], + "author": "", + "license": "ISC", + "description": "" } diff --git a/server.js b/server.js new file mode 100644 index 0000000000000000000000000000000000000000..17dc3df2166d35960ba610e2408eb7000c234b1a --- /dev/null +++ b/server.js @@ -0,0 +1,82 @@ +// server.js +const express = require('express'); +const http = require('http'); +const cors = require('cors'); +const { Server } = require('socket.io'); + +const app = express(); +app.use(cors({ origin: "http://localhost:5173" })); // Autorise le client Vite +const server = http.createServer(app); +const io = new Server(server, { + cors: { + origin: "http://localhost:5173", + methods: ["GET", "POST"] + } +}); + +// Objet pour stocker les joueurs par salle +const players = {}; + +// Optionnel : servir des fichiers statiques +app.use(express.static('public')); + +io.on('connection', (socket) => { + console.log('Nouvelle connexion : ' + socket.id); + + socket.on('joinGame', (data) => { + const { room, playerInfo } = data; + socket.join(room); + + // Initialiser la salle si nécessaire + if (!players[room]) { + players[room] = {}; + } + // Enregistrer le joueur + players[room][socket.id] = playerInfo; + console.log(`${socket.id} a rejoint la salle ${room}`); + + // Envoyer à la socket nouvellement connectée la liste des joueurs déjà présents + const existing = []; + for (const id in players[room]) { + if (id !== socket.id) { + existing.push({ id, playerInfo: players[room][id] }); + } + } + socket.emit('existingPlayers', existing); + + // Notifier les autres joueurs de l'arrivée du nouveau joueur + socket.to(room).emit('newPlayer', { id: socket.id, playerInfo }); + + // Vérifier le nombre de joueurs dans la salle + const roomSize = Object.keys(players[room]).length; + if (roomSize >= 2) { + io.to(room).emit('readyToStart', { room }); + } + }); + + socket.on('playerUpdate', (data) => { + const { room, playerData } = data; + if (players[room]) { + players[room][socket.id] = playerData; + } + socket.to(room).emit('playerUpdate', { id: socket.id, playerData }); + }); + + socket.on('shoot', (data) => { + const { room, shootData } = data; + socket.to(room).emit('shoot', { id: socket.id, shootData }); + }); + + socket.on('disconnect', () => { + console.log('Déconnexion : ' + socket.id); + socket.rooms.forEach((room) => { + if (room !== socket.id && players[room]) { + delete players[room][socket.id]; + socket.to(room).emit('playerDisconnected', { id: socket.id }); + } + }); + }); +}); + +const PORT = process.env.PORT || 3000; +server.listen(PORT, () => console.log(`Serveur démarré sur le port ${PORT}`)); diff --git a/src/index.js b/src/index.js new file mode 100644 index 0000000000000000000000000000000000000000..e9be771c2901a0c84fa339088441d89f204464a6 --- /dev/null +++ b/src/index.js @@ -0,0 +1,456 @@ +// src/index.js + +// Import des modules depuis npm +import * as THREE from 'three'; +import { io } from 'socket.io-client'; + +// ------------------------------- +// 1. Configuration de Socket.IO +// ------------------------------- +const socket = io('http://localhost:3000'); // Se connecter au serveur Socket.IO + +let currentRoom = null; // Code de la salle +let gameStarted = false; // Indique que la partie a démarré + +// ------------------------------- +// 2. Références à l'interface HTML +// ------------------------------- +const startMenu = document.getElementById('startMenu'); +const createGameButton = document.getElementById('createGameButton'); +const joinGameButton = document.getElementById('joinGameButton'); +const roomIdInput = document.getElementById('roomIdInput'); +const shareLinkDiv = document.getElementById('shareLink'); +const shareLinkInput = document.getElementById('shareLinkInput'); +const timerDiv = document.getElementById('timer'); +const healthDisplay = document.getElementById('healthDisplay'); + +// ------------------------------- +// 3. Paramètres du jeu +// ------------------------------- +const playerSpeed = 10; // Vitesse de déplacement du joueur +const zoneShrinkRate = 0.2; // Taux de rétrécissement de la safe zone (unités/s) +const matchDuration = 300; // 5 minutes = 300 s +let matchTimeLeft = matchDuration; +let lastTime = performance.now() / 1000; + +// Pour suivre les touches (déplacement) +const keysPressed = {}; +window.addEventListener('keydown', (event) => { keysPressed[event.code] = true; }); +window.addEventListener('keyup', (event) => { keysPressed[event.code] = false; }); + +// Seuil de collision balle/personnage et dégâts +const collisionThreshold = 1.5; +const bulletDamage = 20; + +// ------------------------------- +// 4. Variables de jeu et stockage +// ------------------------------- +let myPlayer = null; // { mesh, health, speed } +const otherPlayers = {}; // Mapping socketID => { mesh, health } +const bullets = []; // Tableau des balles, chaque balle : { mesh, velocity, lifeTime, ownerId } +const obstacles = []; // Tableau des obstacles + +// ------------------------------- +// 5. Initialisation de Three.js et de la scène +// ------------------------------- +const scene = new THREE.Scene(); +scene.background = new THREE.Color(0x87ceeb); + +const camera = new THREE.PerspectiveCamera( + 75, + window.innerWidth / window.innerHeight, + 0.1, + 1000 +); + +// Création du renderer +const renderer = new THREE.WebGLRenderer({ antialias: true }); +renderer.setSize(window.innerWidth, window.innerHeight); +document.body.appendChild(renderer.domElement); + +// Éclairage pour un bon rendu +const ambientLight = new THREE.AmbientLight(0xffffff, 0.5); +scene.add(ambientLight); +const hemiLight = new THREE.HemisphereLight(0xffffff, 0x444444, 1); +hemiLight.position.set(0, 200, 0); +scene.add(hemiLight); +const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8); +directionalLight.position.set(50, 50, 50); +scene.add(directionalLight); + +// ------------------------------- +// 6. Configuration de la caméra troisième personne +// (Rotation horizontale uniquement, angle vertical verrouillé) +// ------------------------------- +let cameraYaw = 0; // Rotation horizontale (en radians) +const cameraDistance = 10; // Distance de la caméra par rapport au joueur +const cameraHeight = 5; // Hauteur fixe de la caméra +const cameraSensitivity = 0.002; + +// Activer le pointer lock sur le canvas +renderer.domElement.addEventListener('click', () => { + renderer.domElement.requestPointerLock(); +}); + +// Utiliser document pour capturer les mouvements de la souris +document.addEventListener('mousemove', (event) => { + if (document.pointerLockElement === renderer.domElement) { + // Ajouter le mouvement horizontal pour augmenter le yaw + cameraYaw += event.movementX * cameraSensitivity; + } +}); + +// Met à jour la position de la caméra derrière le joueur +function updateCamera() { + if (!myPlayer) return; + const offset = new THREE.Vector3(); + offset.x = cameraDistance * Math.sin(cameraYaw); + offset.z = cameraDistance * Math.cos(cameraYaw); + camera.position.copy(myPlayer.mesh.position) + .add(new THREE.Vector3(0, cameraHeight, 0)) + .sub(offset); + camera.lookAt(myPlayer.mesh.position); +} + +// ------------------------------- +// 7. Création du sol et de la safe zone +// ------------------------------- +// Sol agrandi +const groundGeometry = new THREE.PlaneGeometry(200, 200, 32, 32); +const groundMaterial = new THREE.MeshLambertMaterial({ color: 0x90ee90, side: THREE.DoubleSide }); +const ground = new THREE.Mesh(groundGeometry, groundMaterial); +ground.rotation.x = -Math.PI / 2; +scene.add(ground); + +// Effet organique sur le sol +const posAttr = groundGeometry.attributes.position; +for (let i = 0; i < posAttr.count; i++) { + const x = posAttr.getX(i); + posAttr.setX(i, x + (Math.random() - 0.5) * 2); + posAttr.setZ(i, (Math.random() - 0.5) * 2); +} +groundGeometry.computeVertexNormals(); + +// Safe zone agrandie (sphère en wireframe) +let zoneRadius = 100; +let safeZoneGeometry = new THREE.SphereGeometry(zoneRadius, 32, 32); +const safeZoneMaterial = new THREE.MeshBasicMaterial({ + color: 0xff0000, + opacity: 0.3, + transparent: true, + wireframe: true +}); +const safeZone = new THREE.Mesh(safeZoneGeometry, safeZoneMaterial); +safeZone.position.set(0, 0, 0); +scene.add(safeZone); + +// ------------------------------- +// 8. Création des obstacles +// ------------------------------- +function initObstacles() { + // Créer 5 obstacles (murs) sur la map + for (let i = 0; i < 5; i++) { + const width = 10 + Math.random() * 5; + const height = 5 + Math.random() * 5; + const depth = 5 + Math.random() * 5; + const geometry = new THREE.BoxGeometry(width, height, depth); + const material = new THREE.MeshLambertMaterial({ color: 0x888888 }); + const obstacle = new THREE.Mesh(geometry, material); + // Positionner les obstacles aléatoirement sur le sol (dans une zone raisonnable) + obstacle.position.set((Math.random() - 0.5) * 150, height / 2, (Math.random() - 0.5) * 150); + scene.add(obstacle); + obstacles.push(obstacle); + } +} + +// ------------------------------- +// 9. Fonctions d'initialisation et de jeu +// ------------------------------- +// Crée le joueur local dans une zone de spawn à l'intérieur de la safe zone +function initMyPlayer() { + if (myPlayer) return; + const playerGeometry = new THREE.SphereGeometry(1.5, 32, 32); + const material = new THREE.MeshLambertMaterial({ color: 0x00ff00 }); + const mesh = new THREE.Mesh(playerGeometry, material); + // Spawn aléatoire dans un carré de [-50, 50] pour x et z + mesh.position.set((Math.random() - 0.5) * 100, 1.5, (Math.random() - 0.5) * 100); + scene.add(mesh); + myPlayer = { mesh, health: 100, speed: playerSpeed }; + updateHealthDisplay(); +} + +// Met à jour l'affichage de la santé sous forme de cœurs (chaque cœur = 20 points) +function updateHealthDisplay() { + if (!myPlayer) return; + const heartCount = Math.floor(myPlayer.health / 20); + healthDisplay.innerText = "♥".repeat(heartCount); +} + +// Vérifie les collisions entre balles et personnages ou obstacles +function checkBulletCollisions(delta) { + for (let i = bullets.length - 1; i >= 0; i--) { + const bullet = bullets[i]; + // Collision avec le joueur local (si ce n'est pas lui qui a tiré) + if (bullet.ownerId !== socket.id && myPlayer) { + const dist = bullet.mesh.position.distanceTo(myPlayer.mesh.position); + if (dist < collisionThreshold) { + myPlayer.health -= bulletDamage; + if (myPlayer.health < 0) myPlayer.health = 0; + updateHealthDisplay(); + scene.remove(bullet.mesh); + bullets.splice(i, 1); + if (myPlayer.health <= 0) { + alert("Perdu !"); + scene.remove(myPlayer.mesh); + myPlayer = null; + socket.disconnect(); + window.location.reload(); + } + continue; + } + } + // Collision avec les autres joueurs + for (const id in otherPlayers) { + if (bullet.ownerId === id) continue; + const other = otherPlayers[id]; + const dist = bullet.mesh.position.distanceTo(other.mesh.position); + if (dist < collisionThreshold) { + other.health -= bulletDamage; + if (other.health < 0) other.health = 0; + scene.remove(bullet.mesh); + bullets.splice(i, 1); + if (other.health <= 0) { + scene.remove(other.mesh); + delete otherPlayers[id]; + } + break; + } + } + // Collision avec les obstacles + for (const obstacle of obstacles) { + const box = new THREE.Box3().setFromObject(obstacle); + if (box.containsPoint(bullet.mesh.position)) { + scene.remove(bullet.mesh); + bullets.splice(i, 1); + break; + } + } + } +} + +// Boucle d'animation principale +function animate() { + if (!gameStarted) return; + requestAnimationFrame(animate); + + const now = performance.now() / 1000; + const delta = now - lastTime; + lastTime = now; + + // Mise à jour du timer + matchTimeLeft -= delta; + if (matchTimeLeft < 0) matchTimeLeft = 0; + const minutes = Math.floor(matchTimeLeft / 60); + const seconds = Math.floor(matchTimeLeft % 60); + timerDiv.innerText = `Temps: ${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}`; + + // Rétrécissement progressif de la safe zone + if (zoneRadius > 5) { + zoneRadius -= delta * zoneShrinkRate; + safeZone.geometry.dispose(); + safeZone.geometry = new THREE.SphereGeometry(zoneRadius, 32, 32); + } + + // Dégâts si le joueur est en dehors de la safe zone + if (myPlayer) { + const playerDistance = myPlayer.mesh.position.distanceTo(new THREE.Vector3(0, 0, 0)); + if (playerDistance > zoneRadius) { + myPlayer.health -= 5 * delta; + if (myPlayer.health < 0) myPlayer.health = 0; + updateHealthDisplay(); + if (myPlayer.health <= 0) { + alert("Perdu !"); + scene.remove(myPlayer.mesh); + myPlayer = null; + socket.disconnect(); + window.location.reload(); + } + } + } + + // Déplacement du joueur adapté à l'orientation de la caméra + let moveDir = new THREE.Vector3(); + const forward = new THREE.Vector3(Math.sin(cameraYaw), 0, Math.cos(cameraYaw)); + // Pour obtenir le vecteur "right" correct, on le définit comme (-cos(cameraYaw), 0, sin(cameraYaw)) + const right = new THREE.Vector3(Math.cos(cameraYaw), 0, -Math.sin(cameraYaw)); + if (keysPressed['KeyW']) moveDir.add(forward); + if (keysPressed['KeyS']) moveDir.sub(forward); + if (keysPressed['KeyA']) moveDir.add(right); + if (keysPressed['KeyD']) moveDir.sub(right); + if (moveDir.length() > 0) { + moveDir.normalize(); + myPlayer.mesh.position.add(moveDir.multiplyScalar(myPlayer.speed * delta)); + } + + // Envoyer la mise à jour de la position au serveur + if (myPlayer) { + socket.emit('playerUpdate', { + room: currentRoom, + playerData: { position: myPlayer.mesh.position, health: myPlayer.health } + }); + } + + // Mise à jour des balles tirées + for (let i = bullets.length - 1; i >= 0; i--) { + const bullet = bullets[i]; + bullet.mesh.position.add(bullet.velocity.clone().multiplyScalar(delta)); + bullet.lifeTime -= delta; + if (bullet.lifeTime <= 0) { + scene.remove(bullet.mesh); + bullets.splice(i, 1); + } + } + + // Vérifier les collisions entre balles et joueurs/obstacles + checkBulletCollisions(delta); + + // Mise à jour de la caméra + updateCamera(); + + renderer.render(scene, camera); +} + +// Démarre la partie côté client +function startGame() { + startMenu.style.display = 'none'; + timerDiv.style.display = 'block'; + healthDisplay.style.display = 'block'; + gameStarted = true; + lastTime = performance.now() / 1000; + // Initialiser les obstacles une fois au début de la partie + initObstacles(); + animate(); +} + +// ------------------------------- +// 10. Gestion du tir via la souris (tir horizontal) +// ------------------------------- +window.addEventListener('mousedown', () => { + if (!gameStarted || !myPlayer) return; + // Obtenir la direction de la caméra et forcer y = 0 pour garantir un tir horizontal + const shootDir = new THREE.Vector3(); + camera.getWorldDirection(shootDir); + shootDir.y = 0; + shootDir.normalize(); + + // Envoyer l'événement de tir au serveur + socket.emit('shoot', { room: currentRoom, shootData: { position: myPlayer.mesh.position.clone(), direction: shootDir.clone() } }); + + // Créer localement la balle avec ownerId = socket.id + createBullet({ position: myPlayer.mesh.position.clone(), direction: shootDir, ownerId: socket.id }); +}); + +// Fonction de création d'une balle +function createBullet(shootData) { + const bulletGeometry = new THREE.SphereGeometry(0.3, 16, 16); + const bulletMaterial = new THREE.MeshLambertMaterial({ color: 0xffff00 }); + const bulletMesh = new THREE.Mesh(bulletGeometry, bulletMaterial); + bulletMesh.position.copy(shootData.position); + scene.add(bulletMesh); + bullets.push({ + mesh: bulletMesh, + velocity: new THREE.Vector3().copy(shootData.direction).multiplyScalar(30), + lifeTime: 2, // Durée de vie en secondes + ownerId: shootData.ownerId + }); +} + +// ------------------------------- +// 11. Gestion des événements Socket.IO +// ------------------------------- +socket.on('connect', () => { + console.log("Connecté au serveur avec l'ID :", socket.id); +}); + +// Lorsqu'un nouveau joueur rejoint la partie (après nous) +socket.on('newPlayer', (data) => { + if (data.id === socket.id) return; + console.log("Nouveau joueur :", data.id); + const playerGeometry = new THREE.SphereGeometry(1.5, 32, 32); + const material = new THREE.MeshLambertMaterial({ color: 0xff00ff }); + const mesh = new THREE.Mesh(playerGeometry, material); + mesh.position.copy(data.playerInfo.position); + scene.add(mesh); + otherPlayers[data.id] = { mesh, health: data.playerInfo.health }; +}); + +// Réception de la liste des joueurs déjà présents dans la salle +socket.on('existingPlayers', (playersArray) => { + playersArray.forEach((data) => { + if (data.id === socket.id) return; + console.log("Joueur existant :", data.id); + const playerGeometry = new THREE.SphereGeometry(1.5, 32, 32); + const material = new THREE.MeshLambertMaterial({ color: 0xff00ff }); + const mesh = new THREE.Mesh(playerGeometry, material); + mesh.position.copy(data.playerInfo.position); + scene.add(mesh); + otherPlayers[data.id] = { mesh, health: data.playerInfo.health }; + }); +}); + +// Mise à jour de la position d'un joueur distant +socket.on('playerUpdate', (data) => { + if (data.id === socket.id) return; + if (otherPlayers[data.id]) { + otherPlayers[data.id].mesh.position.copy(data.playerData.position); + } +}); + +// Lorsqu'un joueur se déconnecte +socket.on('playerDisconnected', (data) => { + console.log("Joueur déconnecté :", data.id); + if (otherPlayers[data.id]) { + scene.remove(otherPlayers[data.id].mesh); + delete otherPlayers[data.id]; + } +}); + +// Réception d'un tir d'un autre joueur +socket.on('shoot', (data) => { + if (data.id === socket.id) return; + createBullet({ position: data.shootData.position, direction: data.shootData.direction, ownerId: data.id }); +}); + +// L'événement "readyToStart" indique que la salle compte au moins 2 joueurs +socket.on('readyToStart', (data) => { + console.log("Salle", data.room, ": 2 joueurs connectés. Démarrage de la partie !"); + startGame(); +}); + +// ------------------------------- +// 12. Gestion du menu de démarrage +// ------------------------------- +createGameButton.addEventListener('click', () => { + if (!myPlayer) initMyPlayer(); + currentRoom = Math.random().toString(36).substr(2, 6); + shareLinkDiv.style.display = 'block'; + shareLinkInput.value = window.location.origin + '?room=' + currentRoom; + socket.emit('joinGame', { room: currentRoom, playerInfo: { position: myPlayer.mesh.position, health: 100 } }); +}); + +joinGameButton.addEventListener('click', () => { + if (!myPlayer) initMyPlayer(); + const room = roomIdInput.value.trim(); + if (!room) return; + currentRoom = room; + socket.emit('joinGame', { room: currentRoom, playerInfo: { position: myPlayer.mesh.position, health: 100 } }); +}); + +// ------------------------------- +// 13. Gestion du redimensionnement de la fenêtre +// ------------------------------- +window.addEventListener('resize', () => { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + renderer.setSize(window.innerWidth, window.innerHeight); +});