Skip to content

Commit 178f17c

Browse files
authoredJan 31, 2025··
Merge pull request #8 from CharcherGit/main
Add forum TV MV game dev, and early alpha furbo shiatl.
2 parents 57516d5 + 7617a18 commit 178f17c

File tree

10 files changed

+1077
-40
lines changed

10 files changed

+1077
-40
lines changed
 

‎index.html

+93-13
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,7 @@
159159
bottom: 90px;
160160
width: 450px;
161161
height: 400px;
162-
background: rgba(0, 0, 0, 0.9);
162+
background: rgba(0, 0, 0, 0.65);
163163
color: white;
164164
border-radius: 16px;
165165
padding: 15px;
@@ -433,6 +433,37 @@
433433
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
434434
}
435435

436+
@media (max-width: 1366px) {
437+
#chat-box {
438+
width: 420px;
439+
height: 380px;
440+
left: 20px;
441+
bottom: 85px;
442+
}
443+
444+
#chat-container {
445+
width: 550px;
446+
}
447+
}
448+
449+
@media (max-width: 1024px) {
450+
#chat-box {
451+
width: 380px;
452+
height: 320px;
453+
left: 15px;
454+
bottom: 85px;
455+
}
456+
457+
#chat-container {
458+
width: 450px;
459+
}
460+
461+
.chat-message {
462+
font-size: 13.5px;
463+
padding: 5px 10px;
464+
}
465+
}
466+
436467
@media (max-width: 768px) {
437468
.model-options {
438469
grid-template-columns: repeat(2, 1fr);
@@ -441,6 +472,28 @@
441472
.model-preview {
442473
height: 160px;
443474
}
475+
476+
#chat-box {
477+
width: 320px;
478+
height: 300px;
479+
left: 10px;
480+
bottom: 80px;
481+
}
482+
483+
#chat-container {
484+
width: 400px;
485+
}
486+
487+
.chat-message {
488+
font-size: 13px;
489+
padding: 5px 8px;
490+
line-height: 1.25;
491+
}
492+
493+
#chat-input {
494+
padding: 12px 18px;
495+
font-size: 14px;
496+
}
444497
}
445498

446499
@media (max-width: 480px) {
@@ -451,6 +504,29 @@
451504
.model-preview {
452505
height: 200px;
453506
}
507+
508+
#chat-box {
509+
width: calc(100% - 20px);
510+
height: 250px;
511+
left: 10px;
512+
right: 10px;
513+
bottom: 70px;
514+
}
515+
516+
#chat-container {
517+
width: calc(100% - 20px);
518+
}
519+
520+
#chat-input {
521+
padding: 10px 15px;
522+
font-size: 13px;
523+
}
524+
525+
.chat-message {
526+
font-size: 12.5px;
527+
padding: 4px 8px;
528+
line-height: 1.2;
529+
}
454530
}
455531
</style>
456532
</head>
@@ -499,31 +575,35 @@ <h4>Elige tu personaje:</h4>
499575
<script type="module" src="/src/ui/EmotesUI.js"></script>
500576
<script type="module" src="/src/main.js"></script>
501577
<script>
502-
document.addEventListener('DOMContentLoaded', () => {
578+
// Esperar a que el DOM esté completamente cargado
579+
window.addEventListener('DOMContentLoaded', () => {
503580
const startButton = document.getElementById('start-button');
504581
const usernameInput = document.getElementById('username-input');
582+
const modelOptions = document.querySelectorAll('.model-option');
505583

506584
// Deshabilitar el botón inicialmente
507585
startButton.disabled = true;
508586

587+
// Función para verificar si se puede habilitar el botón
509588
function checkEnableButton() {
510-
const hasUsername = usernameInput.value.trim().length > 0;
511-
const hasModel = document.querySelector('.model-option.selected') !== null;
512-
startButton.disabled = !(hasUsername && hasModel);
589+
const username = usernameInput.value.trim();
590+
const selectedModel = document.querySelector('.model-option.selected');
591+
startButton.disabled = username.length === 0 || !selectedModel;
513592
}
514593

515-
// Verificar cuando se escribe el nombre
516-
usernameInput.addEventListener('input', checkEnableButton);
517-
518-
// Verificar cuando se selecciona un modelo usando el evento personalizado
519-
window.addEventListener('modelSelected', () => {
594+
// Manejar la entrada de texto en el campo de nombre
595+
usernameInput.addEventListener('input', () => {
520596
checkEnableButton();
521597
});
522598

523-
// También verificar cuando se hace clic en las opciones de modelo
524-
document.querySelectorAll('.model-option').forEach(option => {
599+
// Manejar la selección de modelo
600+
modelOptions.forEach(option => {
525601
option.addEventListener('click', () => {
526-
setTimeout(checkEnableButton, 0); // Usar setTimeout para asegurar que se ejecute después de que el modelo se haya actualizado
602+
// Remover la selección previa
603+
modelOptions.forEach(opt => opt.classList.remove('selected'));
604+
// Seleccionar el nuevo modelo
605+
option.classList.add('selected');
606+
checkEnableButton();
527607
});
528608
});
529609
});

‎package.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@
1515
"express": "^4.18.2",
1616
"three": "^0.160.0",
1717
"uuid": "^9.0.1",
18-
"ws": "^8.16.0"
18+
"ws": "^8.16.0",
19+
"puppeteer": "^21.7.0"
1920
},
2021
"devDependencies": {
2122
"concurrently": "^8.2.2",
620 KB
Binary file not shown.

‎server.js

+248-19
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,236 @@ const app = express();
55
const { WebSocketServer } = require('ws');
66
const http = require('http');
77
const { v4: uuidv4 } = require('uuid');
8+
const puppeteer = require('puppeteer');
9+
const cors = require('cors');
10+
const fetch = (...args) => import('node-fetch').then(({default: fetch}) => fetch(...args));
811

9-
// Crear servidor HTTP
12+
// Create HTTP server first
1013
const server = http.createServer(app);
1114

12-
// Crear servidor WebSocket usando el servidor HTTP
15+
// Enable CORS for all routes
16+
app.use(cors());
17+
18+
// Serve static files from the dist directory
19+
app.use(express.static(path.join(__dirname, 'dist')));
20+
21+
// Cache durations
22+
const FORUM_CACHE_DURATION = 30 * 1000; // 30 seconds for forum data
23+
const IMAGE_CACHE_DURATION = 5 * 60 * 1000; // 5 minutes for images
24+
25+
// Cache for storing images temporarily
26+
const imageCache = new Map();
27+
28+
// Clean up expired cache entries periodically
29+
setInterval(() => {
30+
const now = Date.now();
31+
for (const [key, value] of imageCache.entries()) {
32+
if (now - value.timestamp > IMAGE_CACHE_DURATION) {
33+
imageCache.delete(key);
34+
}
35+
}
36+
}, 60000); // Check every minute
37+
38+
// Image proxy endpoint
39+
app.get('/proxy-image', async (req, res) => {
40+
try {
41+
const imageUrl = req.query.url;
42+
if (!imageUrl) {
43+
return res.status(400).send('No image URL provided');
44+
}
45+
46+
const response = await fetch(imageUrl, {
47+
headers: {
48+
'Referer': 'https://www.mediavida.com/',
49+
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
50+
}
51+
});
52+
53+
if (!response.ok) {
54+
return res.status(response.status).send('Failed to fetch image');
55+
}
56+
57+
const contentType = response.headers.get('content-type');
58+
res.setHeader('Content-Type', contentType);
59+
res.setHeader('Cache-Control', 'public, max-age=3600'); // Cache for 1 hour
60+
61+
response.body.pipe(res);
62+
} catch (error) {
63+
console.error('Error proxying image:', error);
64+
res.status(500).send('Error fetching image');
65+
}
66+
});
67+
68+
// Cache for forum posts
69+
let forumPostsCache = [];
70+
let lastFetchTime = 0;
71+
72+
// Function to fetch forum posts
73+
async function fetchForumPosts() {
74+
let browser = null;
75+
try {
76+
browser = await puppeteer.launch({
77+
headless: 'new',
78+
args: ['--no-sandbox', '--disable-setuid-sandbox']
79+
});
80+
const page = await browser.newPage();
81+
82+
// Set a reasonable timeout
83+
await page.setDefaultNavigationTimeout(30000);
84+
85+
await page.goto('https://www.mediavida.com/foro/gamedev/gamedev-taberna-ahora-nuestro-propio-habbo-gamedev-718219/live', {
86+
waitUntil: 'networkidle0'
87+
});
88+
89+
const posts = await page.evaluate(() => {
90+
const postElements = document.querySelectorAll('#posts-wrap .post');
91+
return Array.from(postElements).slice(0, 10).map(post => {
92+
// Username extraction
93+
const username = post.querySelector('.autor')?.textContent?.trim() || '';
94+
95+
// Avatar extraction - try multiple selectors and sources
96+
let avatar = '';
97+
const avatarImg = post.querySelector('.avatar img, .avatar');
98+
if (avatarImg) {
99+
// Try data-src first (lazy loading), then src
100+
avatar = avatarImg.getAttribute('data-src') || avatarImg.getAttribute('src') || '';
101+
102+
// If it's a relative URL, make it absolute
103+
if (avatar && !avatar.startsWith('http')) {
104+
avatar = 'https://www.mediavida.com' + (avatar.startsWith('/') ? '' : '/') + avatar;
105+
}
106+
107+
// Replace any default avatar with empty string
108+
if (avatar.includes('pix.gif') || avatar.includes('default_avatar')) {
109+
avatar = '';
110+
}
111+
}
112+
113+
// Message extraction
114+
const messageEl = post.querySelector('.post-contents');
115+
const message = messageEl ? messageEl.textContent.trim() : '';
116+
117+
// Post number extraction
118+
const postNum = post.getAttribute('data-num') || '';
119+
120+
return {
121+
username,
122+
avatar,
123+
message,
124+
postNum,
125+
timestamp: Date.now()
126+
};
127+
});
128+
});
129+
130+
console.log('Fetched posts with avatars:', posts.map(p => ({ username: p.username, avatar: p.avatar })));
131+
forumPostsCache = posts;
132+
lastFetchTime = Date.now();
133+
return posts;
134+
} catch (error) {
135+
console.error('Error fetching forum posts:', error);
136+
return forumPostsCache; // Return cached posts on error
137+
} finally {
138+
if (browser) {
139+
await browser.close();
140+
}
141+
}
142+
}
143+
144+
// Initialize cache on startup
145+
fetchForumPosts().catch(console.error);
146+
147+
// Proxy endpoint for both forum data and images
148+
app.get('/mv-proxy', async (req, res) => {
149+
try {
150+
// If imageUrl is provided, handle image proxy
151+
if (req.query.imageUrl) {
152+
const imageUrl = req.query.imageUrl;
153+
154+
// Check cache first
155+
if (imageCache.has(imageUrl)) {
156+
const cachedImage = imageCache.get(imageUrl);
157+
if (Date.now() - cachedImage.timestamp < IMAGE_CACHE_DURATION) {
158+
res.type(cachedImage.contentType);
159+
return res.send(cachedImage.data);
160+
} else {
161+
imageCache.delete(imageUrl);
162+
}
163+
}
164+
165+
// Download image
166+
const imageResponse = await fetch(imageUrl, {
167+
headers: {
168+
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
169+
'Referer': 'https://www.mediavida.com/',
170+
'Accept': 'image/webp,image/apng,image/*,*/*;q=0.8'
171+
},
172+
timeout: 5000 // 5 second timeout
173+
}).catch(error => {
174+
console.error(`Failed to fetch image ${imageUrl}:`, error);
175+
return null;
176+
});
177+
178+
if (!imageResponse || !imageResponse.ok) {
179+
console.error(`Image fetch failed for ${imageUrl}: ${imageResponse?.status}`);
180+
return res.status(404).send('Image not found');
181+
}
182+
183+
let contentType = imageResponse.headers.get('content-type');
184+
// Ensure we have a valid content type
185+
if (!contentType || !contentType.startsWith('image/')) {
186+
contentType = 'image/jpeg'; // Default to JPEG if no valid content type
187+
}
188+
189+
const buffer = await imageResponse.buffer().catch(error => {
190+
console.error(`Failed to get buffer for ${imageUrl}:`, error);
191+
return null;
192+
});
193+
194+
if (!buffer) {
195+
return res.status(500).send('Failed to process image');
196+
}
197+
198+
// Cache the image
199+
imageCache.set(imageUrl, {
200+
data: buffer,
201+
contentType,
202+
timestamp: Date.now()
203+
});
204+
205+
// Send response with appropriate headers
206+
res.set('Cache-Control', 'public, max-age=300'); // 5 minutes cache
207+
res.set('Content-Type', contentType);
208+
res.send(buffer);
209+
return;
210+
}
211+
212+
// Handle forum data request
213+
if (Date.now() - lastFetchTime > FORUM_CACHE_DURATION) {
214+
console.log('Cache expired, fetching new posts');
215+
await fetchForumPosts();
216+
}
217+
218+
if (forumPostsCache.length === 0) {
219+
console.log('Cache empty, fetching posts');
220+
await fetchForumPosts();
221+
}
222+
223+
console.log(`Sending ${forumPostsCache.length} posts`);
224+
res.json({ posts: forumPostsCache });
225+
226+
} catch (error) {
227+
console.error('Proxy error:', error);
228+
// En caso de error, devolver el caché existente si hay datos
229+
if (forumPostsCache.length > 0) {
230+
res.json({ posts: forumPostsCache });
231+
} else {
232+
res.status(500).json({ error: error.message });
233+
}
234+
}
235+
});
236+
237+
// Create WebSocket server
13238
const wss = new WebSocketServer({ server });
14239

15240
// Servir archivos estáticos desde la carpeta dist
@@ -98,23 +323,6 @@ wss.on('connection', (ws, req) => {
98323
}
99324
});
100325

101-
app.get('/mv-proxy', async (req, res) => {
102-
try {
103-
const mvResponse = await fetch('https://www.mediavida.com/foro/gamedev/gamedev-taberna-ahora-nuestro-propio-habbo-gamedev-718219/live');
104-
const html = await mvResponse.text();
105-
106-
// Configurar headers CORS
107-
res.header('Access-Control-Allow-Origin', '*');
108-
res.header('X-Frame-Options', 'ALLOWALL');
109-
res.header('Content-Security-Policy', "frame-ancestors 'self' *");
110-
111-
res.send(html);
112-
} catch (error) {
113-
console.error('Error fetching MediaVida:', error);
114-
res.status(500).send('Error fetching content');
115-
}
116-
});
117-
118326
ws.on('message', (message) => {
119327
const data = JSON.parse(message);
120328
const user = users.get(ws);
@@ -412,6 +620,18 @@ wss.on('connection', (ws, req) => {
412620
timestamp: Date.now()
413621
};
414622

623+
// Manejar comando de fútbol
624+
if (data.message.toLowerCase() === '/furbo') {
625+
// Cambiar estado del juego
626+
const isGameActive = data.message.toLowerCase() === '/furbo';
627+
broadcastAll({
628+
type: 'startSoccerGame',
629+
initiatorId: user.id,
630+
active: isGameActive
631+
});
632+
return;
633+
}
634+
415635
// Añadir mensaje al historial
416636
ChatLog.push(messageData);
417637

@@ -473,6 +693,15 @@ wss.on('connection', (ws, req) => {
473693
ChatLog.push(welcomeMessage);
474694
broadcastAll(welcomeMessage);
475695
break;
696+
697+
case 'soccerBallUpdate':
698+
// Reenviar la actualización de la pelota a todos los clientes excepto al remitente
699+
broadcast(ws, {
700+
type: 'soccerBallUpdate',
701+
position: data.position,
702+
velocity: data.velocity
703+
});
704+
break;
476705
}
477706
});
478707

‎src/core/Game.js

+35
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { ChatUI } from '@/ui/ChatUI';
99
import { ModelSelector } from '../ui/ModelSelector';
1010
import * as THREE from 'three';
1111
import { World } from '../world/World';
12+
import { SoccerGame } from '../world/SoccerGame';
1213

1314
export class Game {
1415
constructor() {
@@ -21,6 +22,7 @@ export class Game {
2122
this.renderer = new Renderer(this.scene, this.camera);
2223
this.world = new World(this.scene);
2324
this.environment = new Environment(this.scene, this.world);
25+
this.soccerGame = new SoccerGame(this.scene.getScene(), this.world);
2426

2527
// Inicializar UI antes que network
2628
this.ui = {
@@ -86,6 +88,17 @@ export class Game {
8688
this.world.create();
8789
this.environment.create();
8890
this.ui.modelSelector.init();
91+
await this.soccerGame.init();
92+
93+
// Escuchar eventos del juego de fútbol
94+
this.network.on('startSoccerGame', (data) => {
95+
this.soccerGame.startGame(this.players.players);
96+
// Mostrar mensaje en el chat
97+
this.ui.chat.addMessage({
98+
type: 'system',
99+
message: ${this.players.players.get(data.initiatorId).username} ha iniciado un partido de fútbol!`
100+
});
101+
});
89102
}
90103

91104
setupEventListeners() {
@@ -174,6 +187,27 @@ export class Game {
174187
return;
175188
}
176189

190+
// Realizar raycasting para obtener las coordenadas 3D del punto de intersección
191+
this.raycaster = this.raycaster || new THREE.Raycaster();
192+
this.raycaster.setFromCamera(this.mouse, this.camera.getCamera());
193+
const intersects = this.raycaster.intersectObjects(this.scene.getScene().children, true);
194+
195+
// Mostrar coordenadas en el chat si hay una intersección
196+
if (intersects.length > 0) {
197+
const point = intersects[0].point;
198+
const roundedPoint = {
199+
x: Math.round(point.x * 100) / 100,
200+
y: Math.round(point.y * 100) / 100,
201+
z: Math.round(point.z * 100) / 100
202+
};
203+
/*if (this.ui.chat) {
204+
this.ui.chat.addMessage({
205+
type: 'system',
206+
message: `Debug - Click coordinates: x: ${roundedPoint.x}, y: ${roundedPoint.y}, z: ${roundedPoint.z}`
207+
});
208+
}*/
209+
}
210+
177211
// Procesar interacciones con objetos 3D
178212
if (this.highlightedObject) {
179213
if (this.highlightedObject.userData.type === "arcade") {
@@ -258,6 +292,7 @@ export class Game {
258292
update(deltaTime) {
259293
this.players.update(deltaTime);
260294
this.world.update(deltaTime);
295+
this.soccerGame.update(deltaTime);
261296

262297
// Actualizar posición de la cámara para seguir al jugador local
263298
if (this.players.localPlayer) {

‎src/network/NetworkEvents.js

+7-1
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,13 @@ export const NetworkEvents = {
3131

3232
// Eventos del servidor
3333
BALL_POSITION: 'ballPosition',
34-
BALL_SYNC: 'ballSync'
34+
BALL_SYNC: 'ballSync',
35+
36+
// Eventos de fútbol
37+
START_SOCCER_GAME: 'startSoccerGame',
38+
END_SOCCER_GAME: 'endSoccerGame',
39+
SOCCER_BALL_UPDATE: 'soccerBallUpdate',
40+
SOCCER_SCORE_UPDATE: 'soccerScoreUpdate'
3541
};
3642

3743
// También podemos añadir algunas estructuras de datos comunes

‎src/ui/ChatUI.js

+5-3
Original file line numberDiff line numberDiff line change
@@ -55,12 +55,13 @@ export class ChatUI {
5555

5656
// Prevent game controls while typing
5757
this.chatInput.addEventListener('keydown', (e) => {
58-
if (e.key === ' ' || // Space (jump)
58+
if (this.chatActive && (
59+
e.key === ' ' || // Space (jump)
5960
e.key.toLowerCase() === 'w' || // Movement
6061
e.key.toLowerCase() === 'a' ||
6162
e.key.toLowerCase() === 's' ||
62-
e.key.toLowerCase() === 'd') {
63-
e.stopPropagation(); // Prevent the event from bubbling up
63+
e.key.toLowerCase() === 'd')) {
64+
e.stopPropagation(); // Prevent movement while typing
6465
}
6566
});
6667

@@ -71,6 +72,7 @@ export class ChatUI {
7172
this.sendMessage(message);
7273
this.chatInput.value = '';
7374
}
75+
this.chatInput.blur(); // Solo quitamos el foco cuando se envía el mensaje
7476
}
7577
});
7678
}

‎src/world/Bar.js

+392
Large diffs are not rendered by default.

‎src/world/SoccerGame.js

+285
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,285 @@
1+
import * as THREE from 'three';
2+
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader';
3+
4+
export class SoccerGame {
5+
constructor(scene, world) {
6+
this.scene = scene;
7+
this.world = world;
8+
this.stadium = null;
9+
this.ball = null;
10+
this.players = new Map();
11+
this.isActive = false;
12+
this.originalPlayerPositions = new Map();
13+
this.lastUpdateTime = 0;
14+
this.updateInterval = 50;
15+
16+
// Física de la pelota ajustada para ser más divertida
17+
this.ballPhysics = {
18+
velocity: new THREE.Vector3(),
19+
gravity: -15, // Gravedad más rápida para mejor respuesta
20+
friction: 0.98, // Más fricción para mejor control
21+
bounceForce: 0.65, // Rebote más divertido
22+
maxSpeed: 20, // Velocidad máxima aumentada para más dinamismo
23+
kickForce: 8, // Más fuerza de patada base
24+
minVelocity: 0.05, // Se detiene más rápido cuando va lento
25+
airResistance: 0.995 // Menos resistencia del aire para movimientos más fluidos
26+
};
27+
28+
// Colisiones
29+
this.playerColliders = new Map();
30+
this.ballRadius = 0.2;
31+
this.playerRadius = 1.2; // Radio aumentado para colisiones más fáciles
32+
}
33+
34+
async init() {
35+
const loader = new GLTFLoader();
36+
try {
37+
const gltf = await loader.loadAsync('/assets/models/estadio_furbo.glb');
38+
this.stadium = gltf.scene;
39+
40+
// Configurar el estadio
41+
this.stadium.traverse((child) => {
42+
if (child.isMesh) {
43+
child.castShadow = true;
44+
child.receiveShadow = true;
45+
// Optimizar geometrías
46+
if (child.geometry) {
47+
child.geometry.computeBoundingBox();
48+
child.geometry.computeBoundingSphere();
49+
}
50+
}
51+
});
52+
53+
// Posicionar el estadio
54+
this.stadium.position.set(0, 0, -50);
55+
this.stadium.visible = false;
56+
this.scene.add(this.stadium);
57+
58+
// Crear pelota con física
59+
const ballGeometry = new THREE.SphereGeometry(0.2, 32, 32);
60+
const ballMaterial = new THREE.MeshPhongMaterial({
61+
color: 0xffffff,
62+
roughness: 0.4,
63+
metalness: 0.5
64+
});
65+
this.ball = new THREE.Mesh(ballGeometry, ballMaterial);
66+
this.ball.castShadow = true;
67+
this.ball.visible = false;
68+
this.scene.add(this.ball);
69+
70+
// Añadir colisionador esférico para la pelota
71+
this.ballRadius = 0.2;
72+
73+
// Escuchar actualizaciones de red
74+
if (window.game?.network) {
75+
window.game.network.on('soccerBallUpdate', (data) => {
76+
if (!this.isActive) return;
77+
// Solo actualizar si no somos el último que envió la actualización
78+
if (Date.now() - this.lastUpdateTime > this.updateInterval) {
79+
this.ball.position.copy(new THREE.Vector3().fromArray(data.position));
80+
this.ballPhysics.velocity.copy(new THREE.Vector3().fromArray(data.velocity));
81+
}
82+
});
83+
}
84+
85+
} catch (error) {
86+
console.error('Error loading soccer stadium:', error);
87+
}
88+
}
89+
90+
startGame(players) {
91+
if (!this.stadium) return;
92+
93+
this.isActive = true;
94+
this.stadium.visible = true;
95+
this.ball.visible = true;
96+
97+
// Reiniciar física de la pelota con un pequeño impulso inicial
98+
const initialVelocity = new THREE.Vector3(
99+
(Math.random() - 0.5) * 2,
100+
3,
101+
(Math.random() - 0.5) * 2
102+
);
103+
104+
this.ballPhysics.velocity.copy(initialVelocity);
105+
106+
// Guardar posiciones originales y teleportar jugadores
107+
players.forEach((player, id) => {
108+
this.originalPlayerPositions.set(id, {
109+
position: player.position.clone(),
110+
rotation: player.rotation
111+
});
112+
113+
// Posicionar jugadores en el campo
114+
const randomX = (Math.random() - 0.5) * 10;
115+
const randomZ = (Math.random() - 0.5) * 10;
116+
player.position.set(randomX, 0, -50 + randomZ);
117+
this.players.set(id, player);
118+
119+
// Crear colisionador para el jugador
120+
const collider = new THREE.Sphere(player.mesh.position, this.playerRadius);
121+
this.playerColliders.set(id, collider);
122+
});
123+
124+
// Posicionar pelota en el centro
125+
this.ball.position.set(0, 0.2, -50);
126+
}
127+
128+
endGame() {
129+
if (!this.isActive) return;
130+
131+
this.isActive = false;
132+
this.stadium.visible = false;
133+
this.ball.visible = false;
134+
135+
// Restaurar posiciones originales
136+
this.players.forEach((player, id) => {
137+
const originalPos = this.originalPlayerPositions.get(id);
138+
if (originalPos) {
139+
player.position.copy(originalPos.position);
140+
player.rotation = originalPos.rotation;
141+
}
142+
});
143+
144+
this.players.clear();
145+
this.playerColliders.clear();
146+
this.originalPlayerPositions.clear();
147+
}
148+
149+
checkPlayerCollisions() {
150+
this.players.forEach((player, id) => {
151+
const collider = this.playerColliders.get(id);
152+
if (!collider) return;
153+
154+
collider.center.copy(player.mesh.position);
155+
const ballPos = this.ball.position;
156+
const distance = collider.center.distanceTo(ballPos);
157+
const collisionThreshold = collider.radius + this.ballRadius;
158+
159+
if (distance < collisionThreshold) {
160+
const normal = new THREE.Vector3()
161+
.subVectors(ballPos, collider.center)
162+
.normalize();
163+
164+
// Fuerza base más consistente
165+
const relativeVelocity = this.ballPhysics.velocity.length();
166+
const baseForce = 5; // Aumentado para mejor respuesta
167+
this.ballPhysics.velocity.copy(normal)
168+
.multiplyScalar(Math.max(relativeVelocity, baseForce) * this.ballPhysics.bounceForce);
169+
170+
// Más fuerza al golpear cuando el jugador se mueve
171+
if (player.isMoving) {
172+
const playerDirection = new THREE.Vector3(
173+
Math.sin(player.rotation),
174+
0,
175+
Math.cos(player.rotation)
176+
);
177+
178+
const kickForce = this.ballPhysics.kickForce * 1.5;
179+
this.ballPhysics.velocity.add(
180+
playerDirection.multiplyScalar(kickForce)
181+
);
182+
// Elevación más consistente
183+
this.ballPhysics.velocity.y += Math.min(5, kickForce * 0.4);
184+
} else {
185+
// Añadir un poco de elevación incluso cuando no se mueve
186+
this.ballPhysics.velocity.y += 2;
187+
}
188+
189+
// Mover la pelota fuera de la colisión
190+
this.ball.position.copy(collider.center)
191+
.add(normal.multiplyScalar(collisionThreshold * 1.1)); // Un poco más de separación
192+
193+
this.syncBallState();
194+
}
195+
});
196+
}
197+
198+
syncBallState() {
199+
if (!window.game?.network || Date.now() - this.lastUpdateTime < this.updateInterval) return;
200+
201+
window.game.network.send('soccerBallUpdate', {
202+
position: [this.ball.position.x, this.ball.position.y, this.ball.position.z],
203+
velocity: [this.ballPhysics.velocity.x, this.ballPhysics.velocity.y, this.ballPhysics.velocity.z]
204+
});
205+
206+
this.lastUpdateTime = Date.now();
207+
}
208+
209+
update(deltaTime) {
210+
if (!this.isActive || !this.ball) return;
211+
212+
// Aplicar gravedad con resistencia del aire
213+
this.ballPhysics.velocity.y += this.ballPhysics.gravity * deltaTime;
214+
215+
// Aplicar resistencia del aire (afecta más cuando la pelota está en el aire)
216+
if (this.ball.position.y > this.ballRadius) {
217+
this.ballPhysics.velocity.multiplyScalar(Math.pow(this.ballPhysics.airResistance, deltaTime * 60));
218+
}
219+
220+
// Aplicar fricción solo cuando está cerca del suelo
221+
if (this.ball.position.y <= this.ballRadius + 0.1) {
222+
this.ballPhysics.velocity.x *= Math.pow(this.ballPhysics.friction, deltaTime * 60);
223+
this.ballPhysics.velocity.z *= Math.pow(this.ballPhysics.friction, deltaTime * 60);
224+
}
225+
226+
// Detener la pelota si la velocidad es muy baja
227+
if (this.ballPhysics.velocity.length() < this.ballPhysics.minVelocity) {
228+
this.ballPhysics.velocity.set(0, 0, 0);
229+
}
230+
231+
// Limitar velocidad máxima con un poco de margen para movimientos explosivos
232+
const speed = this.ballPhysics.velocity.length();
233+
if (speed > this.ballPhysics.maxSpeed * 1.2) {
234+
this.ballPhysics.velocity.multiplyScalar(this.ballPhysics.maxSpeed * 1.2 / speed);
235+
}
236+
237+
// Actualizar posición
238+
const movement = this.ballPhysics.velocity.clone().multiplyScalar(deltaTime);
239+
this.ball.position.add(movement);
240+
241+
// Hacer rodar la pelota
242+
if (speed > 0.1 && this.ball.position.y <= this.ballRadius + 0.1) {
243+
const axis = new THREE.Vector3(-movement.z, 0, movement.x).normalize();
244+
const angle = speed * deltaTime * 3;
245+
this.ball.rotateOnAxis(axis, angle);
246+
}
247+
248+
// Rebote con el suelo
249+
if (this.ball.position.y < this.ballRadius) {
250+
this.ball.position.y = this.ballRadius;
251+
if (Math.abs(this.ballPhysics.velocity.y) > 0.1) {
252+
this.ballPhysics.velocity.y = -this.ballPhysics.velocity.y * this.ballPhysics.bounceForce;
253+
// Reducir velocidad horizontal en el impacto basado en la fuerza del impacto
254+
const impactForce = Math.abs(this.ballPhysics.velocity.y) / 10;
255+
this.ballPhysics.velocity.x *= (1 - impactForce);
256+
this.ballPhysics.velocity.z *= (1 - impactForce);
257+
this.syncBallState();
258+
} else {
259+
this.ballPhysics.velocity.y = 0;
260+
}
261+
}
262+
263+
// Límites del campo
264+
const fieldLimits = {
265+
x: 15,
266+
z: 25
267+
};
268+
269+
// Rebote con los límites del campo
270+
if (Math.abs(this.ball.position.x) > fieldLimits.x) {
271+
this.ball.position.x = Math.sign(this.ball.position.x) * fieldLimits.x;
272+
this.ballPhysics.velocity.x = -this.ballPhysics.velocity.x * this.ballPhysics.bounceForce;
273+
this.syncBallState();
274+
}
275+
276+
if (Math.abs(this.ball.position.z + 50) > fieldLimits.z) {
277+
this.ball.position.z = -50 + Math.sign(this.ball.position.z + 50) * fieldLimits.z;
278+
this.ballPhysics.velocity.z = -this.ballPhysics.velocity.z * this.ballPhysics.bounceForce;
279+
this.syncBallState();
280+
}
281+
282+
// Comprobar colisiones con jugadores
283+
this.checkPlayerCollisions();
284+
}
285+
}

‎vite.config.js

+10-3
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,17 @@ export default defineConfig({
2121
port: 5148,
2222
host: true,
2323
proxy: {
24-
'/ws': {
25-
target: 'ws://localhost:3000',
26-
ws: true,
24+
'/mv-proxy': {
25+
target: 'http://localhost:3000',
2726
changeOrigin: true
27+
},
28+
'/proxy-image': {
29+
target: 'http://localhost:3000',
30+
changeOrigin: true
31+
},
32+
'/socket.io': {
33+
target: 'http://localhost:3000',
34+
ws: true
2835
}
2936
}
3037
},

0 commit comments

Comments
 (0)
Please sign in to comment.