diff --git a/agent-plan.md b/agent-plan.md index f09e871..f55f244 100644 --- a/agent-plan.md +++ b/agent-plan.md @@ -1,12 +1,22 @@ -# Pocket 5 Soccer Implementation Plan -## Phase 1: MVP Development -- **Task 1**: Setup Laravel backend and establish database connections. -- **Task 2**: Develop basic React components for game UI. -- **Task 3**: Integrate a 3D game engine, ensuring minimal re-rendering issues. -- **Task 4**: Implement single-player mode with AI opponents. -- **Task 5**: Setup landing page and registration/login system. -## Phase 2: Enhanced Multiplayer -- **Task 6**: Expand multiplayer capabilities to support 1v1 matches. -- **Task 7**: Implement flexible team assignment with a mix of AI and human players. -## Phase 3: Advanced Features -- **Task 8**: +# Pocket 5 Soccer Game Plan + +## Phase 1: Core Game Mechanics (Complete) +- [x] Set up basic Three.js scene with a soccer field. +- [x] Create player, AI teammates, and AI opponents. +- [x] Implement player movement controls. +- [x] Implement ball physics and possession logic. +- [x] Basic AI: All AI players chase the ball. + +## Phase 2: Improved AI Behavior +- [x] When a player has the ball, AI teammates spread out in a diamond formation. +- [ ] **Next Steps:** + - [ ] Improve defensive AI: Instead of all chasing the ball, have defenders mark opponent players or cover zones. + - [ ] Goalkeeper AI: Implement a goalkeeper that stays near the goal and tries to block shots. + - [ ] AI Shooting/Passing: Make AI players with the ball decide whether to shoot or pass to a teammate. + +## Phase 3: UI and UX +- [ ] **Next Steps:** + - [ ] Display a scoreboard. + - [ ] Add a game timer. + - [ ] Create a start/reset menu. + - [ ] Add visual indicators for the selected player. \ No newline at end of file diff --git a/assets/js/game.js b/assets/js/game.js index 5917893..bc8d3ef 100644 --- a/assets/js/game.js +++ b/assets/js/game.js @@ -276,6 +276,18 @@ function updateBallPosition() { } 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); @@ -303,10 +315,113 @@ function updateBallPosition() { } } +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