diff --git a/assets/css/custom.css b/assets/css/custom.css index 56bc6ec..b232bf2 100644 --- a/assets/css/custom.css +++ b/assets/css/custom.css @@ -1,108 +1,11 @@ - -body { - font-family: 'Roboto', sans-serif; - background-color: #F8F9FA; - color: #212529; -} - -h1, h2, h3, h4, h5, h6, .navbar-brand { - font-family: 'Poppins', sans-serif; - font-weight: 700; -} - -.navbar { - transition: background-color 0.3s ease-in-out; -} - -.hero { - background: linear-gradient(rgba(0, 0, 0, 0.5), rgba(0, 0, 0, 0.5)), url('https://picsum.photos/seed/soccerhero/1600/900') no-repeat center center; - background-size: cover; +#scoreboard { + position: absolute; + top: 10px; + left: 50%; + transform: translateX(-50%); + font-size: 40px; color: white; - padding: 10rem 0; - text-align: center; -} - -.hero h1 { - font-size: 4rem; - font-weight: 700; - text-shadow: 2px 2px 4px rgba(0,0,0,0.5); -} - -.hero p { - font-size: 1.5rem; - margin-bottom: 2rem; -} - -.btn-primary { - background-color: #2ECC71; - border-color: #2ECC71; - font-weight: 700; - padding: 0.75rem 1.5rem; - transition: background-color 0.2s, border-color 0.2s; -} - -.btn-primary:hover { - background-color: #27ae60; - border-color: #27ae60; -} - -.btn-secondary { - background-color: #3498DB; - border-color: #3498DB; - font-weight: 700; - padding: 0.75rem 1.5rem; - transition: background-color 0.2s, border-color 0.2s; -} - -.btn-secondary:hover { - background-color: #2980b9; - border-color: #2980b9; -} - -section { - padding: 5rem 0; -} - -.section-title { - text-align: center; - margin-bottom: 3rem; - font-size: 2.5rem; -} - -.card { - border: none; - border-radius: 0.5rem; - box-shadow: 0 4px 15px rgba(0,0,0,0.08); - transition: transform 0.2s ease-in-out, box-shadow 0.2s ease-in-out; -} - -.card:hover { - transform: translateY(-5px); - box-shadow: 0 8px 25px rgba(0,0,0,0.12); -} - -.card-title { - color: #2ECC71; -} - -.list-group-item { - border: none; - padding-left: 0; -} - -.control-item { - background-color: #e9ecef; - padding: 1rem; - border-radius: 0.5rem; - margin-bottom: 1rem; -} - -.control-item strong { - color: #3498DB; -} - -footer { - background-color: #212529; - color: white; - padding: 2rem 0; -} + font-family: sans-serif; + font-weight: bold; + text-shadow: 2px 2px 4px #000000; +} \ No newline at end of file diff --git a/assets/js/Ball.js b/assets/js/Ball.js index 79819c5..5731a63 100644 --- a/assets/js/Ball.js +++ b/assets/js/Ball.js @@ -32,12 +32,24 @@ export class Ball { offset = direction.multiplyScalar(ballOffset); } this.mesh.position.copy(this.mesh.possessedBy.position).add(offset); - this.mesh.position.y = 0.8; // Keep it on the ground } else { // Update ball position based on velocity (when not possessed) this.mesh.position.add(this.mesh.velocity); this.mesh.velocity.multiplyScalar(0.97); // Friction } + + // --- Field Boundaries --- + const fieldWidth = 60; + const fieldLength = 100; + const ballRadius = 0.8; + + this.mesh.position.x = Math.max(-fieldWidth / 2 + ballRadius, Math.min(fieldWidth / 2 - ballRadius, this.mesh.position.x)); + this.mesh.position.z = Math.max(-fieldLength / 2 + ballRadius, Math.min(fieldLength / 2 - ballRadius, this.mesh.position.z)); + + + // --- Force 2D movement on the XZ plane --- + this.mesh.position.y = 0.8; + this.mesh.velocity.y = 0; } } diff --git a/assets/js/Team.js b/assets/js/Team.js index ca8037a..9bc7dc8 100644 --- a/assets/js/Team.js +++ b/assets/js/Team.js @@ -13,25 +13,38 @@ export class Team { createTeam() { for (let i = 0; i < this.numberOfPlayers; i++) { - let player; - if (this.isPlayerTeam) { - if (i === 0) { - player = new Player(this.scene, this.color, { x: -15, y: 1.5, z: 0 }, 'player'); - } else { - player = new Player(this.scene, this.color, { - x: -Math.random() * 25 - 5, // Left side - y: 1.5, - z: (Math.random() - 0.5) * 80 - }, 'player'); - } - } else { - player = new Player(this.scene, this.color, { - x: Math.random() * 25 + 5, // Right side - y: 1.5, - z: (Math.random() - 0.5) * 80 - }, 'bot'); - } + const position = this.generatePosition(i); + const player = new Player(this.scene, this.color, position, this.isPlayerTeam ? 'player' : 'bot'); this.players.push(player); } } + + resetPositions() { + for (let i = 0; i < this.players.length; i++) { + const player = this.players[i]; + const position = this.generatePosition(i); + player.mesh.position.set(position.x, position.y, position.z); + player.mesh.velocity.set(0, 0, 0); + } + } + + generatePosition(i) { + if (this.isPlayerTeam) { + if (i === 0) { + return { x: -15, y: 1.5, z: 0 }; + } else { + return { + x: -Math.random() * 25 - 5, // Left side + y: 1.5, + z: (Math.random() - 0.5) * 80 + }; + } + } else { + return { + x: Math.random() * 25 + 5, // Right side + y: 1.5, + z: (Math.random() - 0.5) * 80 + }; + } + } } diff --git a/assets/js/main.js b/assets/js/main.js index 1beb29b..631e469 100644 --- a/assets/js/main.js +++ b/assets/js/main.js @@ -32,6 +32,10 @@ class Game { this.player = this.playerTeam.players[0]; this.initControls(); + this.score = { red: 0, blue: 0 }; + this.redScoreEl = document.getElementById('red-score'); + this.blueScoreEl = document.getElementById('blue-score'); + this.isGoalScored = false; this.animate(); } @@ -78,6 +82,12 @@ class Game { const newPosition = this.player.mesh.position.clone().add(velocity); + // --- Field Boundaries --- + const fieldWidth = 60; + const fieldLength = 100; + newPosition.x = Math.max(-fieldWidth / 2 + playerRadius, Math.min(fieldWidth / 2 - playerRadius, newPosition.x)); + newPosition.z = Math.max(-fieldLength / 2 + playerRadius, Math.min(fieldLength / 2 - playerRadius, newPosition.z)); + // Collision detection with other players let collision = false; for (const otherPlayer of this.allPlayers) { @@ -93,6 +103,9 @@ class Game { this.player.mesh.position.copy(newPosition); } + // --- Force 2D movement on the XZ plane --- + this.player.mesh.position.y = 1; + // Shooting if (this.keyboardState['KeyD']) { this.player.shoot(this.ball); @@ -130,6 +143,12 @@ class Game { const newPosition = aiPlayer.mesh.position.clone().add(velocity); + // --- Field Boundaries --- + const fieldWidth = 60; + const fieldLength = 100; + newPosition.x = Math.max(-fieldWidth / 2 + playerRadius, Math.min(fieldWidth / 2 - playerRadius, newPosition.x)); + newPosition.z = Math.max(-fieldLength / 2 + playerRadius, Math.min(fieldLength / 2 - playerRadius, newPosition.z)); + // Basic collision avoidance with other players let collision = false; for (const otherPlayer of this.allPlayers) { @@ -145,6 +164,9 @@ class Game { aiPlayer.mesh.position.copy(newPosition); aiPlayer.mesh.lastVelocity.copy(velocity); } + + // --- Force 2D movement on the XZ plane --- + aiPlayer.mesh.position.y = 1; } } @@ -214,12 +236,107 @@ class Game { } } + updateScoreboard() { + this.redScoreEl.textContent = this.score.red; + this.blueScoreEl.textContent = this.score.blue; + } + + resetPositions(teamToGetPossession) { + this.ball.mesh.position.set(0, 0.8, 0); + this.ball.mesh.velocity.set(0, 0, 0); + this.ball.mesh.possessedBy = null; + + // Reset player positions + this.playerTeam.resetPositions(); + this.botTeam.resetPositions(); + + // Give possession to the team that was scored on + if (teamToGetPossession) { + // Give ball to the player closest to the center + let closestPlayer = null; + let minDistance = Infinity; + + const team = teamToGetPossession === 'red' ? this.playerTeam : this.botTeam; + + for (const player of team.players) { + const distance = player.mesh.position.length(); + if (distance < minDistance) { + minDistance = distance; + closestPlayer = player; + } + } + if(closestPlayer){ + this.ball.mesh.possessedBy = closestPlayer.mesh; + } + } + } + + handleGoal(scoringTeam) { + if (this.isGoalScored) return; + this.isGoalScored = true; + + if (scoringTeam === 'red') { + this.score.blue++; + } else { + this.score.red++; + } + this.updateScoreboard(); + + const losingTeam = scoringTeam === 'red' ? 'blue' : 'red'; + + setTimeout(() => { + this.resetPositions(losingTeam); + this.isGoalScored = false; + }, 1000); // 1-second delay + } + + checkCollisions() { + if (this.isGoalScored) return; + + const ballPos = this.ball.mesh.position; + const ballRadius = 0.8; // Corrected from this.ball.radius + const fieldWidth = 60; + const fieldLength = 100; + const goalWidth = 20; + + // Sideline collision (bounce) + const hasHitSideline = (Math.abs(ballPos.x) + ballRadius) > (fieldWidth / 2); + if (hasHitSideline) { + this.ball.mesh.velocity.x *= -1; + ballPos.x = (fieldWidth / 2 - ballRadius) * Math.sign(ballPos.x); + this.ball.mesh.velocity.y = 0; // Keep ball on the ground + ballPos.y = 0.8; // Reset y position + } + + const goalLine = fieldLength / 2; + const isWithinGoalPosts = Math.abs(ballPos.x) < goalWidth / 2; + + // Goal detection or back wall bounce + if (Math.abs(ballPos.z) + ballRadius > goalLine) { + if (isWithinGoalPosts) { + // Goal + if (ballPos.z > 0) { + this.handleGoal('blue'); // Blue team scored + } else { + this.handleGoal('red'); // Red team scored + } + } else { + // Bounce off back wall + this.ball.mesh.velocity.z *= -1; + ballPos.z = (goalLine - ballRadius) * Math.sign(ballPos.z); + this.ball.mesh.velocity.y = 0; // Keep ball on the ground + ballPos.y = 0.8; // Reset y position + } + } + } + animate() { requestAnimationFrame(() => this.animate()); this.updatePlayerPosition(); this.updateAIPlayers(); this.updateBall(); + this.checkCollisions(); // Camera follows player from the sideline this.camera.position.z = this.player.mesh.position.z; diff --git a/game.php b/game.php index 80b6015..b42638c 100644 --- a/game.php +++ b/game.php @@ -4,12 +4,16 @@