326 lines
10 KiB
JavaScript
326 lines
10 KiB
JavaScript
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
|
|
} 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
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Animation Loop
|
|
function animate() {
|
|
requestAnimationFrame(animate);
|
|
updatePlayerPosition();
|
|
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(); |