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();