diff --git a/assets/js/Ball.js b/assets/js/Ball.js new file mode 100644 index 0000000..79819c5 --- /dev/null +++ b/assets/js/Ball.js @@ -0,0 +1,43 @@ + +import * as THREE from 'three'; + +export class Ball { + constructor(scene) { + this.scene = scene; + this.createBall(); + } + + createBall() { + const ballGeometry = new THREE.SphereGeometry(0.8, 16, 16); + const ballMaterial = new THREE.MeshStandardMaterial({ color: 0xffffff }); + this.mesh = new THREE.Mesh(ballGeometry, ballMaterial); + this.mesh.position.y = 0.8; + this.mesh.castShadow = true; + this.mesh.possessedBy = null; + this.mesh.velocity = new THREE.Vector3(); + this.mesh.lastStealTime = 0; + this.scene.add(this.mesh); + } + + update() { + const possessionDistance = 1 + 0.8 + 0.5; // player radius + ball radius + buffer + + if (this.mesh.possessedBy) { + const ballOffset = 1.5; // Distance in front of player + let offset = new THREE.Vector3(ballOffset, 0, 0); // Default offset + + // Ball moves with the player who possesses it + if (this.mesh.possessedBy.lastVelocity && this.mesh.possessedBy.lastVelocity.lengthSq() > 0) { + const direction = this.mesh.possessedBy.lastVelocity.clone().normalize(); + 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 + } + } +} diff --git a/assets/js/Field.js b/assets/js/Field.js new file mode 100644 index 0000000..e57fd36 --- /dev/null +++ b/assets/js/Field.js @@ -0,0 +1,94 @@ + +import * as THREE from 'three'; + +export class Field { + constructor(scene) { + this.scene = scene; + this.createField(); + this.createLines(); + this.createGoals(); + } + + createField() { + const fieldGeometry = new THREE.PlaneGeometry(60, 100); + const fieldMaterial = new THREE.MeshStandardMaterial({ color: 0x008000 }); // Green grass + const field = new THREE.Mesh(fieldGeometry, fieldMaterial); + field.rotation.x = -Math.PI / 2; + field.receiveShadow = true; + this.scene.add(field); + } + + createLines() { + const lineMaterial = new THREE.LineBasicMaterial({ color: 0xffffff }); + + // Outer boundary + const boundary = new THREE.BufferGeometry().setFromPoints([ + new THREE.Vector3(-30, 0.01, -50), + new THREE.Vector3(30, 0.01, -50), + new THREE.Vector3(30, 0.01, 50), + new THREE.Vector3(-30, 0.01, 50), + new THREE.Vector3(-30, 0.01, -50) + ]); + const boundaryLine = new THREE.Line(boundary, lineMaterial); + this.scene.add(boundaryLine); + + // Center line + const centerLineGeo = new THREE.BufferGeometry().setFromPoints([ + new THREE.Vector3(-30, 0.01, 0), + new THREE.Vector3(30, 0.01, 0) + ]); + const centerLine = new THREE.Line(centerLineGeo, lineMaterial); + this.scene.add(centerLine); + + // Center circle + const centerCirclePoints = []; + const centerCircleRadius = 8; + for (let i = 0; i <= 64; i++) { + const theta = (i / 64) * Math.PI * 2; + centerCirclePoints.push(new THREE.Vector3(Math.cos(theta) * centerCircleRadius, 0.01, Math.sin(theta) * centerCircleRadius)); + } + const centerCircleGeo = new THREE.BufferGeometry().setFromPoints(centerCirclePoints); + const centerCircleLine = new THREE.Line(centerCircleGeo, lineMaterial); + this.scene.add(centerCircleLine); + + // Penalty Boxes + const penaltyBox1Geo = new THREE.BufferGeometry().setFromPoints([ + new THREE.Vector3(15, 0.01, -50), + new THREE.Vector3(15, 0.01, -35), + new THREE.Vector3(-15, 0.01, -35), + new THREE.Vector3(-15, 0.01, -50), + new THREE.Vector3(15, 0.01, -50) + ]); + const penaltyBox1 = new THREE.Line(penaltyBox1Geo, lineMaterial); + this.scene.add(penaltyBox1); + + const penaltyBox2Geo = new THREE.BufferGeometry().setFromPoints([ + new THREE.Vector3(15, 0.01, 50), + new THREE.Vector3(15, 0.01, 35), + new THREE.Vector3(-15, 0.01, 35), + new THREE.Vector3(-15, 0.01, 50), + new THREE.Vector3(15, 0.01, 50) + ]); + const penaltyBox2 = new THREE.Line(penaltyBox2Geo, lineMaterial); + this.scene.add(penaltyBox2); + } + + createGoals() { + const goalMaterial = new THREE.MeshStandardMaterial({ color: 0xCCCCCC, metalness: 0.8, roughness: 0.2 }); + + const goal1 = new THREE.Group(); + const post1_1 = new THREE.Mesh(new THREE.BoxGeometry(1, 5, 1), goalMaterial); + post1_1.position.set(-10, 2.5, 0); + const post1_2 = new THREE.Mesh(new THREE.BoxGeometry(1, 5, 1), goalMaterial); + post1_2.position.set(10, 2.5, 0); + const crossbar1 = new THREE.Mesh(new THREE.BoxGeometry(21, 1, 1), goalMaterial); + crossbar1.position.set(0, 5, 0); + goal1.add(post1_1, post1_2, crossbar1); + goal1.position.z = -50; + this.scene.add(goal1); + + const goal2 = goal1.clone(); + goal2.position.z = 50; + this.scene.add(goal2); + } +} diff --git a/assets/js/Player.js b/assets/js/Player.js new file mode 100644 index 0000000..fc97855 --- /dev/null +++ b/assets/js/Player.js @@ -0,0 +1,64 @@ + +import * as THREE from 'three'; + +export class Player { + constructor(scene, color, position, team) { + this.scene = scene; + this.color = color; + this.position = position; + this.team = team; + this.createPlayer(); + } + + createPlayer() { + const playerGeometry = new THREE.CapsuleGeometry(1, 2, 4, 8); + const playerMaterial = new THREE.MeshStandardMaterial({ color: this.color }); + this.mesh = new THREE.Mesh(playerGeometry, playerMaterial); + this.mesh.position.set(this.position.x, this.position.y, this.position.z); + this.mesh.castShadow = true; + this.mesh.lastVelocity = new THREE.Vector3(); + this.mesh.team = this.team; + this.scene.add(this.mesh); + } + + shoot(ball) { + if (ball.mesh.possessedBy === this.mesh) { + ball.mesh.possessedBy = null; + const shootPower = 1.5; + ball.mesh.velocity.copy(this.mesh.lastVelocity).normalize().multiplyScalar(shootPower); + } + } + + pass(ball, teammates) { + if (ball.mesh.possessedBy === this.mesh) { + ball.mesh.possessedBy = null; + const passPower = 0.8; + const passAngle = Math.PI / 6; // 30 degrees + + let targetTeammate = null; + let minAngle = passAngle; + + const playerDirection = this.mesh.lastVelocity.clone().normalize(); + + for (const teammate of teammates) { + const toTeammate = teammate.mesh.position.clone().sub(this.mesh.position); + const distanceToTeammate = toTeammate.length(); + toTeammate.normalize(); + + const angle = playerDirection.angleTo(toTeammate); + + if (angle < minAngle && distanceToTeammate < 40) { // Max pass distance + minAngle = angle; + targetTeammate = teammate; + } + } + + if (targetTeammate) { + const direction = targetTeammate.mesh.position.clone().sub(this.mesh.position).normalize(); + ball.mesh.velocity.copy(direction).multiplyScalar(passPower); + } else { + ball.mesh.velocity.copy(playerDirection).multiplyScalar(passPower); + } + } + } +} diff --git a/assets/js/Team.js b/assets/js/Team.js new file mode 100644 index 0000000..ca8037a --- /dev/null +++ b/assets/js/Team.js @@ -0,0 +1,37 @@ + +import { Player } from './Player.js'; + +export class Team { + constructor(scene, color, numberOfPlayers, isPlayerTeam) { + this.scene = scene; + this.color = color; + this.numberOfPlayers = numberOfPlayers; + this.isPlayerTeam = isPlayerTeam; + this.players = []; + this.createTeam(); + } + + 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'); + } + this.players.push(player); + } + } +} diff --git a/assets/js/game.js b/assets/js/game.js index bc8d3ef..a2f95c6 100644 --- a/assets/js/game.js +++ b/assets/js/game.js @@ -1,441 +1,2 @@ -import * as THREE from 'three'; -// Basic Scene Setup -const scene = new THREE.Scene(); -scene.background = new THREE.Color(0x87CEEB); // Sky blue background - -const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000); -camera.position.set(45, 25, 0); - -const renderer = new THREE.WebGLRenderer({ antialias: true }); -renderer.setSize(window.innerWidth, window.innerHeight); -renderer.shadowMap.enabled = true; -document.body.appendChild(renderer.domElement); - -// Lighting -const ambientLight = new THREE.AmbientLight(0xffffff, 0.6); -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; -scene.add(directionalLight); - -// Field -const fieldGeometry = new THREE.PlaneGeometry(60, 100); -const fieldMaterial = new THREE.MeshStandardMaterial({ color: 0x008000 }); // Green grass -const field = new THREE.Mesh(fieldGeometry, fieldMaterial); -field.rotation.x = -Math.PI / 2; -field.receiveShadow = true; -scene.add(field); - -// Field Lines -const lineMaterial = new THREE.LineBasicMaterial({ color: 0xffffff }); - -// Outer boundary -const boundary = new THREE.BufferGeometry().setFromPoints([ - new THREE.Vector3(-30, 0.01, -50), - new THREE.Vector3(30, 0.01, -50), - new THREE.Vector3(30, 0.01, 50), - new THREE.Vector3(-30, 0.01, 50), - new THREE.Vector3(-30, 0.01, -50) -]); -const boundaryLine = new THREE.Line(boundary, lineMaterial); -scene.add(boundaryLine); - -// Center line -const centerLineGeo = new THREE.BufferGeometry().setFromPoints([ - new THREE.Vector3(-30, 0.01, 0), - new THREE.Vector3(30, 0.01, 0) -]); -const centerLine = new THREE.Line(centerLineGeo, lineMaterial); -scene.add(centerLine); - -// Center circle -const centerCirclePoints = []; -const centerCircleRadius = 8; -for (let i = 0; i <= 64; i++) { - const theta = (i / 64) * Math.PI * 2; - centerCirclePoints.push(new THREE.Vector3(Math.cos(theta) * centerCircleRadius, 0.01, Math.sin(theta) * centerCircleRadius)); -} -const centerCircleGeo = new THREE.BufferGeometry().setFromPoints(centerCirclePoints); -const centerCircleLine = new THREE.Line(centerCircleGeo, lineMaterial); -scene.add(centerCircleLine); - -// Penalty Boxes -const penaltyBox1Geo = new THREE.BufferGeometry().setFromPoints([ - new THREE.Vector3(15, 0.01, -50), - new THREE.Vector3(15, 0.01, -35), - new THREE.Vector3(-15, 0.01, -35), - new THREE.Vector3(-15, 0.01, -50), - new THREE.Vector3(15, 0.01, -50) -]); -const penaltyBox1 = new THREE.Line(penaltyBox1Geo, lineMaterial); -scene.add(penaltyBox1); - -const penaltyBox2Geo = new THREE.BufferGeometry().setFromPoints([ - new THREE.Vector3(15, 0.01, 50), - new THREE.Vector3(15, 0.01, 35), - new THREE.Vector3(-15, 0.01, 35), - new THREE.Vector3(-15, 0.01, 50), - new THREE.Vector3(15, 0.01, 50) -]); -const penaltyBox2 = new THREE.Line(penaltyBox2Geo, lineMaterial); -scene.add(penaltyBox2); - -// Goals -const goalGeometry = new THREE.BoxGeometry(20, 5, 2); -const goalMaterial = new THREE.MeshStandardMaterial({ color: 0xCCCCCC, metalness: 0.8, roughness: 0.2 }); - -const goal1 = new THREE.Group(); -const post1_1 = new THREE.Mesh(new THREE.BoxGeometry(1, 5, 1), goalMaterial); -post1_1.position.set(-10, 2.5, 0); -const post1_2 = new THREE.Mesh(new THREE.BoxGeometry(1, 5, 1), goalMaterial); -post1_2.position.set(10, 2.5, 0); -const crossbar1 = new THREE.Mesh(new THREE.BoxGeometry(21, 1, 1), goalMaterial); -crossbar1.position.set(0, 5, 0); -goal1.add(post1_1, post1_2, crossbar1); -goal1.position.z = -50; -scene.add(goal1); - -const goal2 = goal1.clone(); -goal2.position.z = 50; -scene.add(goal2); - -// Player -const playerGeometry = new THREE.CapsuleGeometry(1, 2, 4, 8); -const playerMaterial = new THREE.MeshStandardMaterial({ color: 0xff0000 }); // Red player -const player = new THREE.Mesh(playerGeometry, playerMaterial); -player.position.y = 1.5; // Stand on the field -player.castShadow = true; -player.lastVelocity = new THREE.Vector3(); -player.team = 'player'; -scene.add(player); - -// Teammates -const teammateMaterial = new THREE.MeshStandardMaterial({ color: 0xff0000 }); // Red teammates -const teammates = []; -for (let i = 0; i < 3; i++) { - const teammate = new THREE.Mesh(playerGeometry, teammateMaterial); - teammate.position.set( - (Math.random() - 0.5) * 50, - 1.5, - (Math.random() - 0.5) * 80 - ); - teammate.castShadow = true; - teammate.team = 'player'; - teammate.lastVelocity = new THREE.Vector3(); - scene.add(teammate); - teammates.push(teammate); -} - - -// Simple Bot Players (Static) -const botMaterial = new THREE.MeshStandardMaterial({ color: 0x0000ff }); // Blue bots -const bots = []; -for (let i = 0; i < 4; i++) { - const bot = new THREE.Mesh(playerGeometry, botMaterial); - bot.position.set( - (Math.random() - 0.5) * 50, - 1.5, - (Math.random() - 0.5) * 80 - ); - bot.castShadow = true; - bot.team = 'bot'; - bot.lastVelocity = new THREE.Vector3(); - scene.add(bot); - bots.push(bot); -} - -const allPlayers = [player, ...teammates, ...bots]; - -// Ball -const ballGeometry = new THREE.SphereGeometry(0.8, 16, 16); -const ballMaterial = new THREE.MeshStandardMaterial({ color: 0xffffff }); -const ball = new THREE.Mesh(ballGeometry, ballMaterial); -ball.position.y = 0.8; -ball.castShadow = true; -ball.possessedBy = null; -ball.velocity = new THREE.Vector3(); -scene.add(ball); - - -// Player Controls -const keyboardState = {}; -window.addEventListener('keydown', (e) => { keyboardState[e.code] = true; }); -window.addEventListener('keyup', (e) => { keyboardState[e.code] = false; }); - -const playerSpeed = 0.2; -const playerRadius = 1; // Approximate radius for collision - -function updatePlayerPosition() { - const velocity = new THREE.Vector3(); - - if (keyboardState['ArrowUp']) { - velocity.x -= playerSpeed; - } - if (keyboardState['ArrowDown']) { - velocity.x += playerSpeed; - } - if (keyboardState['ArrowLeft']) { - velocity.z += playerSpeed; - } - if (keyboardState['ArrowRight']) { - velocity.z -= playerSpeed; - } - - if (velocity.lengthSq() > 0) { - player.lastVelocity.copy(velocity); - } - - const newPosition = player.position.clone().add(velocity); - - // Collision detection with other players - let collision = false; - for (const otherPlayer of allPlayers) { - if (otherPlayer === player) continue; - const distance = newPosition.distanceTo(otherPlayer.position); - if (distance < playerRadius * 2) { - collision = true; - break; - } - } - - if (!collision) { - player.position.copy(newPosition); - } - - // Shooting - if (keyboardState['KeyD']) { - shoot(); - } - - // Passing - if (keyboardState['KeyS']) { - pass(); - } -} - -function shoot() { - if (ball.possessedBy === player) { - ball.possessedBy = null; - const shootPower = 1.5; - ball.velocity.copy(player.lastVelocity).normalize().multiplyScalar(shootPower); - } -} - -function pass() { - if (ball.possessedBy === player) { - ball.possessedBy = null; - const passPower = 0.8; - const passAngle = Math.PI / 6; // 30 degrees - - let targetTeammate = null; - let minAngle = passAngle; - - const playerDirection = player.lastVelocity.clone().normalize(); - - for (const teammate of teammates) { - const toTeammate = teammate.position.clone().sub(player.position); - const distanceToTeammate = toTeammate.length(); - toTeammate.normalize(); - - const angle = playerDirection.angleTo(toTeammate); - - if (angle < minAngle && distanceToTeammate < 40) { // Max pass distance - minAngle = angle; - targetTeammate = teammate; - } - } - - if (targetTeammate) { - const direction = targetTeammate.position.clone().sub(player.position).normalize(); - ball.velocity.copy(direction).multiplyScalar(passPower); - } else { - ball.velocity.copy(playerDirection).multiplyScalar(passPower); - } - } -} - - -function updateBallPosition() { - const possessionDistance = playerRadius + 0.8 + 0.5; // player radius + ball radius + buffer - - if (ball.possessedBy) { - const ballOffset = 1.5; // Distance in front of player - let offset = new THREE.Vector3(ballOffset, 0, 0); // Default offset - - // Ball moves with the player who possesses it - if (ball.possessedBy.lastVelocity && ball.possessedBy.lastVelocity.lengthSq() > 0) { - const direction = ball.possessedBy.lastVelocity.clone().normalize(); - offset = direction.multiplyScalar(ballOffset); - } - ball.position.copy(ball.possessedBy.position).add(offset); - ball.position.y = 0.8; // Keep it on the ground - - // Check for steals - const stealRadius = playerRadius * 2.2; - for (const p of allPlayers) { - if (p === ball.possessedBy) continue; - - const distanceToBallOwner = p.position.distanceTo(ball.possessedBy.position); - if (distanceToBallOwner < stealRadius) { - ball.possessedBy = p; - break; - } - } - } else { - // Update ball position based on velocity (when not possessed) - ball.position.add(ball.velocity); - ball.velocity.multiplyScalar(0.97); // Friction - - // Ball collision with players - for (const p of allPlayers) { - const distance = ball.position.distanceTo(p.position); - if (distance < playerRadius + 0.8) { - const normal = ball.position.clone().sub(p.position).normalize(); - ball.velocity.reflect(normal).multiplyScalar(0.8); - } - } - - - // Check for gaining possession - for (const p of allPlayers) { - const distanceToPlayer = ball.position.distanceTo(p.position); - if (distanceToPlayer < possessionDistance && ball.velocity.lengthSq() < 0.1) { - ball.possessedBy = p; - ball.velocity.set(0, 0, 0); - break; // Only one player can possess the ball at a time - } - } - } -} - -const aiSpeed = 0.1; - -function getDiamondFormation(ballOwner, allTeammates) { - const formation = []; - const spacing = 15; - - const ownerPos = ballOwner.position; - let playerDirection = ballOwner.lastVelocity.clone(); - if (playerDirection.lengthSq() === 0) { // If player is not moving - // Default direction towards opponent's goal - playerDirection = (ballOwner.team === 'player') ? new THREE.Vector3(0, 0, 1) : new THREE.Vector3(0, 0, -1); - } - playerDirection.normalize(); - - - // Vector perpendicular to player direction on the XZ plane - const perpendicularDirection = new THREE.Vector3(-playerDirection.z, 0, playerDirection.x); - - const basePositions = [ - playerDirection.clone().multiplyScalar(spacing), // Ahead - playerDirection.clone().multiplyScalar(-spacing * 0.8), // Behind (closer) - perpendicularDirection.clone().multiplyScalar(spacing), // Right - perpendicularDirection.clone().multiplyScalar(-spacing) // Left - ]; - - // Assign available positions to teammates - for (let i = 0; i < allTeammates.length; i++) { - if(basePositions[i]) { - formation.push(ownerPos.clone().add(basePositions[i])); - } - } - return formation; -} - -function updateAIPlayers() { - const ballOwner = ball.possessedBy; - - // Get all AI players for team 'player' and team 'bot' - const playerTeamAIs = teammates; - const botTeamAIs = bots; - - // Determine which AI players are teammates of the ball owner - let formationAIs = []; - if (ballOwner) { - if (ballOwner.team === 'player') { - formationAIs = playerTeamAIs.filter(p => p !== ballOwner); - } else if (ballOwner.team === 'bot') { - formationAIs = botTeamAIs.filter(p => p !== ballOwner); - } - } - // To have a consistent assignment, sort them by id - formationAIs.sort((a,b) => a.id - b.id); - - let formationPositions = []; - if (ballOwner && formationAIs.length > 0) { - formationPositions = getDiamondFormation(ballOwner, formationAIs); - } - - for (const aiPlayer of allPlayers) { - if (aiPlayer === player) continue; // Skip user-controlled player - - let targetPosition; - const formationIndex = formationAIs.indexOf(aiPlayer); - - if (ballOwner === aiPlayer) { - // This AI has the ball, move towards the opponent's goal - targetPosition = new THREE.Vector3(0, 0, aiPlayer.team === 'player' ? 50 : -50); - } else if (ballOwner && aiPlayer.team === ballOwner.team && formationIndex !== -1) { - // This AI is a teammate of the ball owner, get into formation - if(formationPositions[formationIndex]){ - targetPosition = formationPositions[formationIndex]; - } else { - targetPosition = ball.position; // Fallback - } - } else { - // This AI is an opponent, or no one has the ball. Chase the ball. - targetPosition = ball.position; - } - - const direction = targetPosition.clone().sub(aiPlayer.position).normalize(); - const velocity = direction.multiplyScalar(aiSpeed); - - const newPosition = aiPlayer.position.clone().add(velocity); - - // Basic collision avoidance with other players - let collision = false; - for (const otherPlayer of allPlayers) { - if (otherPlayer === aiPlayer) continue; - const distance = newPosition.distanceTo(otherPlayer.position); - if (distance < playerRadius * 2) { - collision = true; - break; - } - } - - if (!collision) { - aiPlayer.position.copy(newPosition); - aiPlayer.lastVelocity.copy(velocity); - } - } -} - -// Animation Loop -function animate() { - requestAnimationFrame(animate); - updatePlayerPosition(); - updateAIPlayers(); - updateBallPosition(); - - // Camera follows player from the sideline - camera.position.z = player.position.z; - camera.lookAt(player.position); - - renderer.render(scene, camera); -} - -// Handle Window Resize -window.addEventListener('resize', () => { - camera.aspect = window.innerWidth / window.innerHeight; - camera.updateProjectionMatrix(); - renderer.setSize(window.innerWidth, window.innerHeight); -}); - -animate(); \ No newline at end of file +import './main.js'; diff --git a/assets/js/main.js b/assets/js/main.js index 472e1f1..1beb29b 100644 --- a/assets/js/main.js +++ b/assets/js/main.js @@ -1,11 +1,232 @@ -// Smooth scrolling for anchor links -document.querySelectorAll('a[href^="#"]').forEach(anchor => { - anchor.addEventListener('click', function (e) { - e.preventDefault(); +import * as THREE from 'three'; +import { Field } from './Field.js'; +import { Ball } from './Ball.js'; +import { Team } from './Team.js'; - document.querySelector(this.getAttribute('href')).scrollIntoView({ - behavior: 'smooth' - }); - }); -}); +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 = {}; + + this.init(); + } + + 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.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); + + // 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); + } + + // 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); + + // 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); + } + } + } + + 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; + } + } + } + } + } + + animate() { + requestAnimationFrame(() => this.animate()); + + this.updatePlayerPosition(); + this.updateAIPlayers(); + this.updateBall(); + + // 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); + } +} + +new Game();