import * as THREE from 'three'; import { Field } from 'app/field'; import { Ball } from 'app/ball'; import { Team } from 'app/team'; class Game { constructor() { this.scene = new THREE.Scene(); this.camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000); this.renderer = new THREE.WebGLRenderer({ antialias: true }); this.keyboardState = {}; } init() { this.scene.background = new THREE.Color(0x87CEEB); // Sky blue background this.camera.position.set(45, 25, 0); this.renderer.setSize(window.innerWidth, window.innerHeight); this.renderer.shadowMap.enabled = true; document.body.appendChild(this.renderer.domElement); this.initLighting(); this.field = new Field(this.scene); this.ball = new Ball(this.scene); this.playerTeam = new Team(this.scene, 0xff0000, 4, true); this.botTeam = new Team(this.scene, 0x0000ff, 4, false); this.allPlayers = [...this.playerTeam.players, ...this.botTeam.players]; 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.goalCooldown = 0; this.timer = 30; this.gameOver = false; this.timerEl = document.getElementById('timer'); this.gameOverModal = document.getElementById('game-over-modal'); this.finalScoreEl = document.getElementById('final-score'); this.winnerEl = document.getElementById('winner'); this.restartButton = document.getElementById('restart-button'); this.restartButton.addEventListener('click', () => this.restartGame()); this.animate(); } initLighting() { const ambientLight = new THREE.AmbientLight(0xffffff, 0.6); this.scene.add(ambientLight); const directionalLight = new THREE.DirectionalLight(0xffffff, 1.2); directionalLight.position.set(20, 30, 20); directionalLight.castShadow = true; directionalLight.shadow.camera.top = 20; directionalLight.shadow.camera.bottom = -20; directionalLight.shadow.camera.left = -20; directionalLight.shadow.camera.right = 20; this.scene.add(directionalLight); } initControls() { window.addEventListener('keydown', (e) => { this.keyboardState[e.code] = true; }); window.addEventListener('keyup', (e) => { this.keyboardState[e.code] = false; }); } updatePlayerPosition() { const playerSpeed = 0.2; const playerRadius = 1; // Approximate radius for collision const velocity = new THREE.Vector3(); if (this.keyboardState['ArrowUp']) { velocity.x -= playerSpeed; } if (this.keyboardState['ArrowDown']) { velocity.x += playerSpeed; } if (this.keyboardState['ArrowLeft']) { velocity.z += playerSpeed; } if (this.keyboardState['ArrowRight']) { velocity.z -= playerSpeed; } if (velocity.lengthSq() > 0) { this.player.mesh.lastVelocity.copy(velocity); } 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) { if (otherPlayer === this.player) continue; const distance = newPosition.distanceTo(otherPlayer.mesh.position); if (distance < playerRadius * 2) { collision = true; break; } } if (!collision) { 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); } // Passing if (this.keyboardState['KeyS']) { this.player.pass(this.ball, this.playerTeam.players.slice(1)); } } updateAIPlayers() { const aiSpeed = 0.1; const playerRadius = 1; for (const aiPlayer of this.allPlayers) { if (aiPlayer === this.player) continue; // Skip user-controlled player let targetPosition; const ballOwner = this.ball.mesh.possessedBy; if (ballOwner === aiPlayer.mesh) { // This AI has the ball, move towards the opponent's goal targetPosition = new THREE.Vector3(0, 0, aiPlayer.mesh.team === 'player' ? 50 : -50); } else if (ballOwner && aiPlayer.mesh.team === ballOwner.team) { // This AI is a teammate of the ball owner, get into formation targetPosition = ballOwner.position.clone().add(new THREE.Vector3(10, 0, 10)); } else { // This AI is an opponent, or no one has the ball. Chase the ball. targetPosition = this.ball.mesh.position; } const direction = targetPosition.clone().sub(aiPlayer.mesh.position).normalize(); const velocity = direction.multiplyScalar(aiSpeed); 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) { if (otherPlayer === aiPlayer) continue; const distance = newPosition.distanceTo(otherPlayer.mesh.position); if (distance < playerRadius * 2) { collision = true; break; } } if (!collision) { aiPlayer.mesh.position.copy(newPosition); aiPlayer.mesh.lastVelocity.copy(velocity); } // --- Force 2D movement on the XZ plane --- aiPlayer.mesh.position.y = 1; } } updateBall() { this.ball.update(); const playerRadius = 1; const possessionDistance = playerRadius + 0.8 + 0.5; const STEAL_COOLDOWN = 1000; // Ball collision with players for (const p of this.allPlayers) { const distance = this.ball.mesh.position.distanceTo(p.mesh.position); if (distance < playerRadius + 0.8) { const normal = this.ball.mesh.position.clone().sub(p.mesh.position).normalize(); this.ball.mesh.velocity.reflect(normal).multiplyScalar(0.8); } } // Check for gaining possession if (!this.ball.mesh.possessedBy) { for (const p of this.allPlayers) { const distanceToPlayer = this.ball.mesh.position.distanceTo(p.mesh.position); if (distanceToPlayer < possessionDistance && this.ball.mesh.velocity.lengthSq() < 0.1) { this.ball.mesh.possessedBy = p.mesh; this.ball.mesh.velocity.set(0, 0, 0); this.ball.mesh.lastStealTime = Date.now(); break; // Only one player can possess the ball at a time } } } // Check for steals const stealRadius = playerRadius * 2.2; const now = Date.now(); const ballOwner = this.ball.mesh.possessedBy; if (ballOwner && now - this.ball.mesh.lastStealTime > STEAL_COOLDOWN) { for (const p of this.allPlayers) { if (p.mesh === ballOwner) continue; const distanceToBallOwner = p.mesh.position.distanceTo(ballOwner.position); if (distanceToBallOwner < stealRadius) { const owner = ballOwner; const stealer = p.mesh; let canSteal = true; // Default to allow steal if (owner.lastVelocity.lengthSq() > 0) { // Check if the stealer is roughly in front of the owner const ownerDirection = owner.lastVelocity.clone().normalize(); const toStealer = stealer.position.clone().sub(owner.position).normalize(); const angle = ownerDirection.angleTo(toStealer); if (angle >= Math.PI / 2) { // If stealer is not in front (90 degree FOV) canSteal = false; } } if (canSteal) { this.ball.mesh.possessedBy = stealer; this.ball.mesh.lastStealTime = now; break; } } } } } 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; this.goalCooldown = 60; // 60 frames ~ 1 second if (scoringTeam === 'red') { this.score.blue++; } else { this.score.red++; } this.updateScoreboard(); const losingTeam = scoringTeam === 'red' ? 'blue' : 'red'; this.resetPositions(losingTeam); } updateTimer() { if (this.gameOver) return; this.timer -= 1 / 60; // Assuming 60 FPS this.timerEl.textContent = Math.ceil(this.timer); if (this.timer <= 0) { this.endGame(); } } endGame() { this.gameOver = true; this.gameOverModal.style.display = 'flex'; let winner; if (this.score.red > this.score.blue) { winner = 'Red Team Wins!'; } else if (this.score.blue > this.score.red) { winner = 'Blue Team Wins!'; } else { winner = "It's a draw!"; } this.finalScoreEl.textContent = `Final Score: ${this.score.red} - ${this.score.blue}`; this.winnerEl.textContent = winner; } restartGame() { this.gameOver = false; this.timer = 30; this.score = { red: 0, blue: 0 }; this.updateScoreboard(); this.gameOverModal.style.display = 'none'; this.resetPositions(); } checkCollisions() { 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()); if (this.gameOver) { this.renderer.render(this.scene, this.camera); return; } this.updateTimer(); if (this.goalCooldown > 0) { this.goalCooldown--; if (this.goalCooldown === 0) { this.isGoalScored = false; } } this.updatePlayerPosition(); this.updateAIPlayers(); this.updateBall(); if (!this.isGoalScored) { this.checkCollisions(); } // Camera follows player from the sideline this.camera.position.z = this.player.mesh.position.z; this.camera.lookAt(this.player.mesh.position); this.renderer.render(this.scene, this.camera); } } const game = new Game(); const startGameBtn = document.getElementById('start-game-btn'); const tutorialModal = document.getElementById('tutorial-modal'); function startGame() { tutorialModal.style.display = 'none'; game.init(); } startGameBtn.addEventListener('click', startGame);