Add stealing mechanics and refactor frontend js
This commit is contained in:
parent
364f853d0e
commit
c82e73843b
43
assets/js/Ball.js
Normal file
43
assets/js/Ball.js
Normal file
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
94
assets/js/Field.js
Normal file
94
assets/js/Field.js
Normal file
@ -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);
|
||||
}
|
||||
}
|
||||
64
assets/js/Player.js
Normal file
64
assets/js/Player.js
Normal file
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
37
assets/js/Team.js
Normal file
37
assets/js/Team.js
Normal file
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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();
|
||||
import './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();
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user