Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
67991b8a3e | ||
|
|
e6f6732eaa | ||
|
|
794387fa46 | ||
|
|
c82e73843b | ||
|
|
364f853d0e | ||
|
|
db2d07f244 | ||
|
|
5681888e69 | ||
|
|
2f1c0bce8c |
10
.htaccess
10
.htaccess
@ -16,3 +16,13 @@ RewriteRule ^(.+?)/?$ $1.php [L]
|
|||||||
# 2) Optional: strip trailing slash for non-directories (keeps .php links working)
|
# 2) Optional: strip trailing slash for non-directories (keeps .php links working)
|
||||||
RewriteCond %{REQUEST_FILENAME} !-d
|
RewriteCond %{REQUEST_FILENAME} !-d
|
||||||
RewriteRule ^(.+)/$ $1 [R=301,L]
|
RewriteRule ^(.+)/$ $1 [R=301,L]
|
||||||
|
|
||||||
|
<FilesMatch "\.(js)$">
|
||||||
|
FileETag None
|
||||||
|
<IfModule mod_headers.c>
|
||||||
|
Header unset ETag
|
||||||
|
Header set Cache-Control "max-age=0, no-cache, no-store, must-revalidate"
|
||||||
|
Header set Pragma "no-cache"
|
||||||
|
Header set Expires "Wed, 11 Jan 1984 05:00:00 GMT"
|
||||||
|
</IfModule>
|
||||||
|
</FilesMatch>
|
||||||
22
agent-plan.md
Normal file
22
agent-plan.md
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
# 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.
|
||||||
19
agent.md
Normal file
19
agent.md
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
# Pocket 5 Soccer Project Specification
|
||||||
|
## Overview
|
||||||
|
Pocket 5 Soccer is a 5v5 multiplayer soccer game, designed for quick gaming sessions on mobile and desktop platforms. The game supports both single-player and multiplayer modes, with AI-controlled players filling in any non-human positions.
|
||||||
|
## User Roles
|
||||||
|
- **Player**: The sole user role, enjoying playing against AI or other human players.
|
||||||
|
## User Stories
|
||||||
|
- **Single Player Mode**: Players can play against AI-controlled teams.
|
||||||
|
- **Multiplayer Mode**: Players can compete against each other, with the game eventually supporting up to 8 human players in a match.
|
||||||
|
## Public Interface
|
||||||
|
- **Landing Page**: Displays a demo and includes a registration/login page leading to the main game.
|
||||||
|
## Technical Stack
|
||||||
|
### Frontend
|
||||||
|
- **React**: For UI components.
|
||||||
|
- **3D Game Engine**: Used directly without React to avoid re-rendering issues, using libraries like Three.js or Babylon.js.
|
||||||
|
### Backend
|
||||||
|
- **Laravel**: Chosen for its robustness and efficient backend capabilities.
|
||||||
|
## No Third-Party Integrations or Automations
|
||||||
|
## Design Aesthetics
|
||||||
|
- Fun, casual web game style for an engaging user experience.
|
||||||
109
assets/css/custom.css
Normal file
109
assets/css/custom.css
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
#scoreboard {
|
||||||
|
position: absolute;
|
||||||
|
top: 10px;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
font-size: 40px;
|
||||||
|
color: white;
|
||||||
|
font-family: sans-serif;
|
||||||
|
font-weight: bold;
|
||||||
|
text-shadow: 2px 2px 4px #000000;
|
||||||
|
}
|
||||||
|
|
||||||
|
#timer {
|
||||||
|
position: absolute;
|
||||||
|
top: 60px; /* Below the scoreboard */
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
font-size: 30px;
|
||||||
|
color: white;
|
||||||
|
font-family: sans-serif;
|
||||||
|
font-weight: bold;
|
||||||
|
text-shadow: 2px 2px 4px #000000;
|
||||||
|
}
|
||||||
|
|
||||||
|
#game-over-modal {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background-color: rgba(0, 0, 0, 0.7);
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
color: white;
|
||||||
|
font-family: sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
#game-over-content {
|
||||||
|
text-align: center;
|
||||||
|
background-color: #333;
|
||||||
|
padding: 40px;
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#game-over-content h2 {
|
||||||
|
font-size: 50px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#game-over-content p {
|
||||||
|
font-size: 24px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#restart-button {
|
||||||
|
margin-top: 20px;
|
||||||
|
padding: 15px 30px;
|
||||||
|
font-size: 20px;
|
||||||
|
cursor: pointer;
|
||||||
|
border: none;
|
||||||
|
border-radius: 5px;
|
||||||
|
background-color: #4CAF50;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
#tutorial-modal {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background-color: rgba(0, 0, 0, 0.7);
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
color: white;
|
||||||
|
font-family: sans-serif;
|
||||||
|
z-index: 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-content {
|
||||||
|
text-align: center;
|
||||||
|
background-color: #333;
|
||||||
|
padding: 40px;
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-content h2 {
|
||||||
|
font-size: 50px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-content p {
|
||||||
|
font-size: 20px;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
#start-game-btn {
|
||||||
|
margin-top: 20px;
|
||||||
|
padding: 15px 30px;
|
||||||
|
font-size: 20px;
|
||||||
|
cursor: pointer;
|
||||||
|
border: none;
|
||||||
|
border-radius: 5px;
|
||||||
|
background-color: #4CAF50;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
54
assets/js/Ball.js
Normal file
54
assets/js/Ball.js
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
} else {
|
||||||
|
// Update ball position based on velocity (when not possessed)
|
||||||
|
this.mesh.position.add(this.mesh.velocity);
|
||||||
|
this.mesh.velocity.multiplyScalar(0.97); // Friction
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Field Boundaries ---
|
||||||
|
const fieldWidth = 60;
|
||||||
|
const fieldLength = 100;
|
||||||
|
const ballRadius = 0.8;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// --- Force 2D movement on the XZ plane ---
|
||||||
|
this.mesh.position.y = 0.8;
|
||||||
|
this.mesh.velocity.y = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
68
assets/js/Player.js
Normal file
68
assets/js/Player.js
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
|
||||||
|
import * as THREE from 'three';
|
||||||
|
|
||||||
|
export class Player {
|
||||||
|
constructor(scene, color, position, type, team) {
|
||||||
|
this.scene = scene;
|
||||||
|
this.color = color;
|
||||||
|
this.velocity = new THREE.Vector3(0, 0, 0);
|
||||||
|
this.type = type;
|
||||||
|
this.team = team;
|
||||||
|
this.radius = 1.5;
|
||||||
|
this.hasBall = false;
|
||||||
|
this.createPlayer(new THREE.Vector3(position.x, position.y, position.z));
|
||||||
|
}
|
||||||
|
|
||||||
|
createPlayer(initialPosition) {
|
||||||
|
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.copy(initialPosition);
|
||||||
|
this.mesh.initialPosition = initialPosition.clone();
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
48
assets/js/Team.js
Normal file
48
assets/js/Team.js
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
|
||||||
|
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++) {
|
||||||
|
const position = this.generatePosition(i);
|
||||||
|
const player = new Player(this.scene, this.color, position, this.isPlayerTeam ? 'player' : 'bot', this);
|
||||||
|
this.players.push(player);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resetPositions() {
|
||||||
|
this.players.forEach(player => {
|
||||||
|
player.mesh.position.copy(player.mesh.initialPosition);
|
||||||
|
player.velocity.set(0, 0, 0);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
generatePosition(i) {
|
||||||
|
if (this.isPlayerTeam) {
|
||||||
|
if (i === 0) {
|
||||||
|
return { x: -15, y: 1.5, z: 0 };
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
x: -Math.random() * 25 - 5, // Left side
|
||||||
|
y: 1.5,
|
||||||
|
z: (Math.random() - 0.5) * 80
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
x: Math.random() * 25 + 5, // Right side
|
||||||
|
y: 1.5,
|
||||||
|
z: (Math.random() - 0.5) * 80
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
2
assets/js/game.js
Normal file
2
assets/js/game.js
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
|
||||||
|
import './main.js';
|
||||||
414
assets/js/main.js
Normal file
414
assets/js/main.js
Normal file
@ -0,0 +1,414 @@
|
|||||||
|
|
||||||
|
import * as THREE from 'three';
|
||||||
|
import { Field } from 'app/field';
|
||||||
|
import { Ball } from 'app/ball';
|
||||||
|
import { Team } from 'app/team';
|
||||||
|
|
||||||
|
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 = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
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.score = { red: 0, blue: 0 };
|
||||||
|
this.redScoreEl = document.getElementById('red-score');
|
||||||
|
this.blueScoreEl = document.getElementById('blue-score');
|
||||||
|
this.isGoalScored = false;
|
||||||
|
this.goalCooldown = 0;
|
||||||
|
this.timer = 30;
|
||||||
|
this.gameOver = false;
|
||||||
|
this.timerEl = document.getElementById('timer');
|
||||||
|
this.gameOverModal = document.getElementById('game-over-modal');
|
||||||
|
this.finalScoreEl = document.getElementById('final-score');
|
||||||
|
this.winnerEl = document.getElementById('winner');
|
||||||
|
this.restartButton = document.getElementById('restart-button');
|
||||||
|
this.restartButton.addEventListener('click', () => this.restartGame());
|
||||||
|
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);
|
||||||
|
|
||||||
|
// --- Field Boundaries ---
|
||||||
|
const fieldWidth = 60;
|
||||||
|
const fieldLength = 100;
|
||||||
|
newPosition.x = Math.max(-fieldWidth / 2 + playerRadius, Math.min(fieldWidth / 2 - playerRadius, newPosition.x));
|
||||||
|
newPosition.z = Math.max(-fieldLength / 2 + playerRadius, Math.min(fieldLength / 2 - playerRadius, newPosition.z));
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Force 2D movement on the XZ plane ---
|
||||||
|
this.player.mesh.position.y = 1;
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
|
||||||
|
// --- Field Boundaries ---
|
||||||
|
const fieldWidth = 60;
|
||||||
|
const fieldLength = 100;
|
||||||
|
newPosition.x = Math.max(-fieldWidth / 2 + playerRadius, Math.min(fieldWidth / 2 - playerRadius, newPosition.x));
|
||||||
|
newPosition.z = Math.max(-fieldLength / 2 + playerRadius, Math.min(fieldLength / 2 - playerRadius, newPosition.z));
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Force 2D movement on the XZ plane ---
|
||||||
|
aiPlayer.mesh.position.y = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateScoreboard() {
|
||||||
|
this.redScoreEl.textContent = this.score.red;
|
||||||
|
this.blueScoreEl.textContent = this.score.blue;
|
||||||
|
}
|
||||||
|
|
||||||
|
resetPositions(teamToGetPossession) {
|
||||||
|
this.ball.mesh.position.set(0, 0.8, 0);
|
||||||
|
this.ball.mesh.velocity.set(0, 0, 0);
|
||||||
|
this.ball.mesh.possessedBy = null;
|
||||||
|
|
||||||
|
// Reset player positions
|
||||||
|
this.playerTeam.resetPositions();
|
||||||
|
this.botTeam.resetPositions();
|
||||||
|
|
||||||
|
// Give possession to the team that was scored on
|
||||||
|
if (teamToGetPossession) {
|
||||||
|
// Give ball to the player closest to the center
|
||||||
|
let closestPlayer = null;
|
||||||
|
let minDistance = Infinity;
|
||||||
|
|
||||||
|
const team = teamToGetPossession === 'red' ? this.playerTeam : this.botTeam;
|
||||||
|
|
||||||
|
for (const player of team.players) {
|
||||||
|
const distance = player.mesh.position.length();
|
||||||
|
if (distance < minDistance) {
|
||||||
|
minDistance = distance;
|
||||||
|
closestPlayer = player;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if(closestPlayer){
|
||||||
|
this.ball.mesh.possessedBy = closestPlayer.mesh;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleGoal(scoringTeam) {
|
||||||
|
if (this.isGoalScored) return;
|
||||||
|
this.isGoalScored = true;
|
||||||
|
this.goalCooldown = 60; // 60 frames ~ 1 second
|
||||||
|
|
||||||
|
if (scoringTeam === 'red') {
|
||||||
|
this.score.blue++;
|
||||||
|
} else {
|
||||||
|
this.score.red++;
|
||||||
|
}
|
||||||
|
this.updateScoreboard();
|
||||||
|
|
||||||
|
const losingTeam = scoringTeam === 'red' ? 'blue' : 'red';
|
||||||
|
this.resetPositions(losingTeam);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateTimer() {
|
||||||
|
if (this.gameOver) return;
|
||||||
|
|
||||||
|
this.timer -= 1 / 60; // Assuming 60 FPS
|
||||||
|
this.timerEl.textContent = Math.ceil(this.timer);
|
||||||
|
|
||||||
|
if (this.timer <= 0) {
|
||||||
|
this.endGame();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
endGame() {
|
||||||
|
this.gameOver = true;
|
||||||
|
this.gameOverModal.style.display = 'flex';
|
||||||
|
|
||||||
|
let winner;
|
||||||
|
if (this.score.red > this.score.blue) {
|
||||||
|
winner = 'Red Team Wins!';
|
||||||
|
} else if (this.score.blue > this.score.red) {
|
||||||
|
winner = 'Blue Team Wins!';
|
||||||
|
} else {
|
||||||
|
winner = "It's a draw!";
|
||||||
|
}
|
||||||
|
|
||||||
|
this.finalScoreEl.textContent = `Final Score: ${this.score.red} - ${this.score.blue}`;
|
||||||
|
this.winnerEl.textContent = winner;
|
||||||
|
}
|
||||||
|
|
||||||
|
restartGame() {
|
||||||
|
this.gameOver = false;
|
||||||
|
this.timer = 30;
|
||||||
|
this.score = { red: 0, blue: 0 };
|
||||||
|
this.updateScoreboard();
|
||||||
|
this.gameOverModal.style.display = 'none';
|
||||||
|
this.resetPositions();
|
||||||
|
}
|
||||||
|
|
||||||
|
checkCollisions() {
|
||||||
|
const ballPos = this.ball.mesh.position;
|
||||||
|
const ballRadius = 0.8; // Corrected from this.ball.radius
|
||||||
|
const fieldWidth = 60;
|
||||||
|
const fieldLength = 100;
|
||||||
|
const goalWidth = 20;
|
||||||
|
|
||||||
|
// Sideline collision (bounce)
|
||||||
|
const hasHitSideline = (Math.abs(ballPos.x) + ballRadius) > (fieldWidth / 2);
|
||||||
|
if (hasHitSideline) {
|
||||||
|
this.ball.mesh.velocity.x *= -1;
|
||||||
|
ballPos.x = (fieldWidth / 2 - ballRadius) * Math.sign(ballPos.x);
|
||||||
|
this.ball.mesh.velocity.y = 0; // Keep ball on the ground
|
||||||
|
ballPos.y = 0.8; // Reset y position
|
||||||
|
}
|
||||||
|
|
||||||
|
const goalLine = fieldLength / 2;
|
||||||
|
const isWithinGoalPosts = Math.abs(ballPos.x) < goalWidth / 2;
|
||||||
|
|
||||||
|
// Goal detection or back wall bounce
|
||||||
|
if (Math.abs(ballPos.z) + ballRadius > goalLine) {
|
||||||
|
if (isWithinGoalPosts) {
|
||||||
|
// Goal
|
||||||
|
if (ballPos.z > 0) {
|
||||||
|
this.handleGoal('blue'); // Blue team scored
|
||||||
|
} else {
|
||||||
|
this.handleGoal('red'); // Red team scored
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Bounce off back wall
|
||||||
|
this.ball.mesh.velocity.z *= -1;
|
||||||
|
ballPos.z = (goalLine - ballRadius) * Math.sign(ballPos.z);
|
||||||
|
this.ball.mesh.velocity.y = 0; // Keep ball on the ground
|
||||||
|
ballPos.y = 0.8; // Reset y position
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
animate() {
|
||||||
|
requestAnimationFrame(() => this.animate());
|
||||||
|
|
||||||
|
if (this.gameOver) {
|
||||||
|
this.renderer.render(this.scene, this.camera);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.updateTimer();
|
||||||
|
|
||||||
|
if (this.goalCooldown > 0) {
|
||||||
|
this.goalCooldown--;
|
||||||
|
if (this.goalCooldown === 0) {
|
||||||
|
this.isGoalScored = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.updatePlayerPosition();
|
||||||
|
this.updateAIPlayers();
|
||||||
|
this.updateBall();
|
||||||
|
if (!this.isGoalScored) {
|
||||||
|
this.checkCollisions();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const game = new Game();
|
||||||
|
|
||||||
|
const startGameBtn = document.getElementById('start-game-btn');
|
||||||
|
const tutorialModal = document.getElementById('tutorial-modal');
|
||||||
|
|
||||||
|
function startGame() {
|
||||||
|
tutorialModal.style.display = 'none';
|
||||||
|
game.init();
|
||||||
|
}
|
||||||
|
|
||||||
|
startGameBtn.addEventListener('click', startGame);
|
||||||
157
index.php
157
index.php
@ -1,131 +1,46 @@
|
|||||||
<?php
|
<!DOCTYPE html>
|
||||||
declare(strict_types=1);
|
|
||||||
@ini_set('display_errors', '1');
|
|
||||||
@error_reporting(E_ALL);
|
|
||||||
@date_default_timezone_set('UTC');
|
|
||||||
|
|
||||||
$phpVersion = PHP_VERSION;
|
|
||||||
$now = date('Y-m-d H:i:s');
|
|
||||||
?>
|
|
||||||
<!doctype html>
|
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>New Style</title>
|
<title>Pocket 5 Soccer</title>
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
<link rel="stylesheet" href="assets/css/custom.css?v=<?php echo time(); ?>">
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;700&display=swap" rel="stylesheet">
|
|
||||||
<style>
|
<style>
|
||||||
:root {
|
body { margin: 0; overflow: hidden; }
|
||||||
--bg-color-start: #6a11cb;
|
canvas { display: block; }
|
||||||
--bg-color-end: #2575fc;
|
|
||||||
--text-color: #ffffff;
|
|
||||||
--card-bg-color: rgba(255, 255, 255, 0.01);
|
|
||||||
--card-border-color: rgba(255, 255, 255, 0.1);
|
|
||||||
}
|
|
||||||
body {
|
|
||||||
margin: 0;
|
|
||||||
font-family: 'Inter', sans-serif;
|
|
||||||
background: linear-gradient(45deg, var(--bg-color-start), var(--bg-color-end));
|
|
||||||
color: var(--text-color);
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
min-height: 100vh;
|
|
||||||
text-align: center;
|
|
||||||
overflow: hidden;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
body::before {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
background-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100" viewBox="0 0 100 100"><path d="M-10 10L110 10M10 -10L10 110" stroke-width="1" stroke="rgba(255,255,255,0.05)"/></svg>');
|
|
||||||
animation: bg-pan 20s linear infinite;
|
|
||||||
z-index: -1;
|
|
||||||
}
|
|
||||||
@keyframes bg-pan {
|
|
||||||
0% { background-position: 0% 0%; }
|
|
||||||
100% { background-position: 100% 100%; }
|
|
||||||
}
|
|
||||||
main {
|
|
||||||
padding: 2rem;
|
|
||||||
}
|
|
||||||
.card {
|
|
||||||
background: var(--card-bg-color);
|
|
||||||
border: 1px solid var(--card-border-color);
|
|
||||||
border-radius: 16px;
|
|
||||||
padding: 2rem;
|
|
||||||
backdrop-filter: blur(20px);
|
|
||||||
-webkit-backdrop-filter: blur(20px);
|
|
||||||
box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.1);
|
|
||||||
}
|
|
||||||
.loader {
|
|
||||||
margin: 1.25rem auto 1.25rem;
|
|
||||||
width: 48px;
|
|
||||||
height: 48px;
|
|
||||||
border: 3px solid rgba(255, 255, 255, 0.25);
|
|
||||||
border-top-color: #fff;
|
|
||||||
border-radius: 50%;
|
|
||||||
animation: spin 1s linear infinite;
|
|
||||||
}
|
|
||||||
@keyframes spin {
|
|
||||||
from { transform: rotate(0deg); }
|
|
||||||
to { transform: rotate(360deg); }
|
|
||||||
}
|
|
||||||
.hint {
|
|
||||||
opacity: 0.9;
|
|
||||||
}
|
|
||||||
.sr-only {
|
|
||||||
position: absolute;
|
|
||||||
width: 1px; height: 1px;
|
|
||||||
padding: 0; margin: -1px;
|
|
||||||
overflow: hidden;
|
|
||||||
clip: rect(0, 0, 0, 0);
|
|
||||||
white-space: nowrap; border: 0;
|
|
||||||
}
|
|
||||||
h1 {
|
|
||||||
font-size: 3rem;
|
|
||||||
font-weight: 700;
|
|
||||||
margin: 0 0 1rem;
|
|
||||||
letter-spacing: -1px;
|
|
||||||
}
|
|
||||||
p {
|
|
||||||
margin: 0.5rem 0;
|
|
||||||
font-size: 1.1rem;
|
|
||||||
}
|
|
||||||
code {
|
|
||||||
background: rgba(0,0,0,0.2);
|
|
||||||
padding: 2px 6px;
|
|
||||||
border-radius: 4px;
|
|
||||||
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
|
||||||
}
|
|
||||||
footer {
|
|
||||||
position: absolute;
|
|
||||||
bottom: 1rem;
|
|
||||||
font-size: 0.8rem;
|
|
||||||
opacity: 0.7;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<main>
|
<div id="tutorial-modal">
|
||||||
<div class="card">
|
<div class="modal-content">
|
||||||
<h1>Analyzing your requirements and generating your website…</h1>
|
<h2>Pocket 5 Soccer</h2>
|
||||||
<div class="loader" role="status" aria-live="polite" aria-label="Applying initial changes">
|
<p>Use Arrow Keys to move. Press S to pass, D to shoot.</p>
|
||||||
<span class="sr-only">Loading…</span>
|
<button id="start-game-btn">Start Game</button>
|
||||||
</div>
|
</div>
|
||||||
<p class="hint"><?= ($_SERVER['HTTP_HOST'] ?? '') === 'appwizzy.com' ? 'AppWiZZy' : 'Flatlogic' ?> AI is collecting your requirements and applying the first changes.</p>
|
|
||||||
<p class="hint">This page will update automatically as the plan is implemented.</p>
|
|
||||||
<p>Runtime: PHP <code><?= htmlspecialchars($phpVersion) ?></code> — UTC <code><?= htmlspecialchars($now) ?></code></p>
|
|
||||||
</div>
|
</div>
|
||||||
</main>
|
<div id="scoreboard">
|
||||||
<footer>
|
<span id="red-score">0</span> - <span id="blue-score">0</span>
|
||||||
Page updated: <?= htmlspecialchars($now) ?> (UTC)
|
</div>
|
||||||
</footer>
|
<div id="timer">30</div>
|
||||||
|
<div id="game-over-modal" style="display: none;">
|
||||||
|
<div id="game-over-content">
|
||||||
|
<h2>Game Over</h2>
|
||||||
|
<p id="final-score"></p>
|
||||||
|
<p id="winner"></p>
|
||||||
|
<button id="restart-button">Restart Game</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<script type="importmap">
|
||||||
|
{
|
||||||
|
"imports": {
|
||||||
|
"three": "https://unpkg.com/three@0.163.0/build/three.module.js",
|
||||||
|
"three/addons/": "https://unpkg.com/three@0.163.0/examples/jsm/",
|
||||||
|
"app/field": "./assets/js/Field.js?v=<?php echo time(); ?>",
|
||||||
|
"app/ball": "./assets/js/Ball.js?v=<?php echo time(); ?>",
|
||||||
|
"app/team": "./assets/js/Team.js?v=<?php echo time(); ?>"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<script type="module" src="assets/js/main.js?v=<?php echo time(); ?>"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
Loading…
x
Reference in New Issue
Block a user