Worms_game / game.js
Andro0s's picture
Update game.js
94b5eb5 verified
// =======================================================
// === Three.js 3D Initialization + Movimiento Avanzado ===
// =======================================================
let scene, camera, renderer, terrain, controls;
let originalWormModel = null;
let players = []; // Array con los jugadores { id, mesh, vel, grounded, bobTime }
const keys = {};
const container = document.getElementById('game-container');
const textureLoader = new THREE.TextureLoader();
// Parámetros
const movementSpeed = 0.7;
const jumpSpeed = 10;
const gravity = 28;
const groundOffset = 5; // separación del gusano respecto a la superficie (no tocar tu modelo)
const bobAmplitude = 0.25;
const bobFrequency = 8;
const clock = new THREE.Clock();
// -----------------------
// Util: obtener altura del terreno en (x,z)
// -----------------------
function getTerrainHeight(x, z) {
if (!terrain || !terrain.geometry) return 0;
const geometry = terrain.geometry;
const posAttr = geometry.attributes.position;
const TERRAIN_SIZE = 150; // debe coincidir con createTerrain
const HALF = TERRAIN_SIZE / 2;
// Normalizar x,z a 0..1
const tx = (x + HALF) / TERRAIN_SIZE;
const tz = (z + HALF) / TERRAIN_SIZE;
// Clamp para evitar fuera del terreno
const nx = Math.max(0, Math.min(1, tx));
const nz = Math.max(0, Math.min(1, tz));
const cols = Math.round(Math.sqrt(posAttr.count));
if (!cols || cols === 0) return 0;
const ix = Math.floor(nx * (cols - 1));
const iz = Math.floor(nz * (cols - 1));
const index = iz * cols + ix;
// Si por alguna razón index fuera de rango, devolver 0
if (index < 0 || index >= posAttr.count) return 0;
return posAttr.getY(index);
}
// -----------------------
// Inicialización
// -----------------------
function init() {
scene = new THREE.Scene();
scene.background = new THREE.Color(0x87ceeb);
camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.set(0, 50, 70);
camera.lookAt(0, 0, 0);
renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
if (container) container.appendChild(renderer.domElement);
else { console.error("No se encontró #game-container"); return; }
const ambientLight = new THREE.AmbientLight(0x404040, 3);
scene.add(ambientLight);
const directionalLight = new THREE.DirectionalLight(0xffffff, 2);
directionalLight.position.set(100, 100, 50);
scene.add(directionalLight);
createTerrain();
loadWormModel(); // cuando termine, clonará y creará players
controls = new THREE.OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
controls.dampingFactor = 0.05;
controls.minDistance = 20;
controls.maxDistance = 120;
window.addEventListener('keydown', onKeyDown);
window.addEventListener('keyup', onKeyUp);
window.addEventListener('resize', onWindowResize);
}
// -----------------------
// Terreno (procedural) — NO toca tu diseño, solo lo crea
// -----------------------
function createTerrain() {
const texture = textureLoader.load(
'grass_texture.jpg',
tex => {
tex.wrapS = THREE.RepeatWrapping;
tex.wrapT = THREE.RepeatWrapping;
tex.repeat.set(10, 10);
},
undefined,
err => console.warn('No cargó textura (CORS?)', err)
);
const TERRAIN_SIZE = 150;
const SEGMENTS = 128; // resolución (más alto = más preciso para alturas)
const geometry = new THREE.PlaneGeometry(TERRAIN_SIZE, TERRAIN_SIZE, SEGMENTS, SEGMENTS);
geometry.rotateX(-Math.PI / 2);
const vertices = geometry.attributes.position.array;
const MAX_ELEVATION = 15;
for (let i = 0; i < vertices.length; i += 3) {
const x = vertices[i] / TERRAIN_SIZE;
const y = vertices[i + 2] / TERRAIN_SIZE;
const elevation =
(Math.sin(x * 10) * 0.5 +
Math.cos(y * 7) * 0.8 +
Math.sin((x + y) * 5) * 0.3) * MAX_ELEVATION;
vertices[i + 1] = elevation;
}
geometry.computeVertexNormals();
terrain = new THREE.Mesh(
geometry,
new THREE.MeshPhongMaterial({ map: texture, side: THREE.DoubleSide })
);
scene.add(terrain);
}
// -----------------------
// Cargar modelo OBJ (una sola vez) y crear dos jugadores clonados
// -----------------------
function loadWormModel() {
const objLoader = new THREE.OBJLoader();
objLoader.load(
'cascabel123.obj',
function (object) {
// Guardar el modelo original (sin añadirlo a escena)
originalWormModel = object;
// Material de prueba pero respetando texturas si existieran
object.traverse(child => {
if (child.type === 'Mesh') {
// Si ya tiene material, lo dejamos; si no, aplicamos uno suave
if (!child.material) {
child.material = new THREE.MeshPhongMaterial({
color: 0x68e079,
specular: 0x555555,
shininess: 30
});
}
}
});
// Crear players: jugador 1 y jugador 2
createPlayerFromModel('player1', -10, 0, 0);
createPlayerFromModel('player2', 10, 0, 0);
console.log("Modelo cargado y jugadores creados.");
},
undefined,
function (error) {
console.error('Error cargando cascabel123.obj', error);
}
);
}
function createPlayerFromModel(id, startX = 0, startZ = 0, startYOffset = 5) {
if (!originalWormModel) {
console.warn("Modelo aún no cargado; reintentar después.");
return;
}
// Clonar profundamente para tener dos instancias independientes
const clone = originalWormModel.clone(true);
// Garantizar materiales en meshes (por si no se clonaron)
clone.traverse(child => {
if (child.type === 'Mesh') {
if (!child.material) {
child.material = new THREE.MeshPhongMaterial({
color: 0x68e079,
specular: 0x555555,
shininess: 30
});
}
child.castShadow = true;
child.receiveShadow = true;
}
});
// Ajustes iniciales sin tocar diseño visible
clone.scale.set(10, 10, 10);
const x = startX, z = startZ;
const terrainH = getTerrainHeight(x, z);
clone.position.set(x, terrainH + groundOffset + startYOffset*0, z);
scene.add(clone);
// Añadir al array de players
players.push({
id,
mesh: clone,
vel: new THREE.Vector3(0, 0, 0),
grounded: false,
bobTime: 0
});
}
// -----------------------
// Teclado robusto (mapea Space y Enter también)
// -----------------------
function onKeyDown(e) {
// e.key a minúsculas; e.code para espacios/enter
const k = (e.key || '').toLowerCase();
if (k) keys[k] = true;
if (e.code === 'Space') keys['space'] = true;
if (e.code === 'Enter') keys['enter'] = true;
// Flechas vienen como 'ArrowUp' etc. mapear a 'arrowup'
if (e.key && e.key.startsWith('Arrow')) keys[e.key.toLowerCase()] = true;
}
function onKeyUp(e) {
const k = (e.key || '').toLowerCase();
if (k) keys[k] = false;
if (e.code === 'Space') keys['space'] = false;
if (e.code === 'Enter') keys['enter'] = false;
if (e.key && e.key.startsWith('Arrow')) keys[e.key.toLowerCase()] = false;
}
// -----------------------
// Lógica de movimiento para ambos jugadores (WASD+Space y Flechas+Enter)
// -----------------------
function handleWormMovement(delta) {
if (!players.length) return;
players.forEach((p, idx) => {
const m = p.mesh;
if (!m) return;
const isPlayer1 = p.id === 'player1';
// Controles por jugador
const forwardKey = isPlayer1 ? 'w' : 'arrowup';
const backKey = isPlayer1 ? 's' : 'arrowdown';
const leftKey = isPlayer1 ? 'a' : 'arrowleft';
const rightKey = isPlayer1 ? 'd' : 'arrowright';
const jumpKey = isPlayer1 ? 'space' : 'enter';
let moved = false;
// Movimiento horizontal plano (X,Z)
if (keys[forwardKey]) {
m.position.z -= movementSpeed;
m.rotation.y = 0;
moved = true;
}
if (keys[backKey]) {
m.position.z += movementSpeed;
m.rotation.y = Math.PI;
moved = true;
}
if (keys[leftKey]) {
m.position.x -= movementSpeed;
m.rotation.y = Math.PI / 2;
moved = true;
}
if (keys[rightKey]) {
m.position.x += movementSpeed;
m.rotation.y = -Math.PI / 2;
moved = true;
}
// Actualizar bob (animación simple) si se mueve
if (moved) {
p.bobTime += delta * bobFrequency;
} else {
p.bobTime = 0; // resetea la oscilación cuando está quieto
}
// Física vertical: gravedad y salto
const terrainY = getTerrainHeight(m.position.x, m.position.z);
const targetGroundY = terrainY + groundOffset;
// Si toca salto y está en tierra, aplicar impulso
if (keys[jumpKey] && p.grounded) {
p.vel.y = jumpSpeed;
p.grounded = false;
// Evitar salto sostenido: limpiar la tecla para que no salte infinito mientras se mantiene pulsado
keys[jumpKey] = false;
if (jumpKey === 'space') keys['space'] = false;
if (jumpKey === 'enter') keys['enter'] = false;
}
// Aplicar gravedad
p.vel.y -= gravity * delta;
m.position.y += p.vel.y * delta;
// Comprobación de colisión con terreno
if (m.position.y <= targetGroundY) {
m.position.y = targetGroundY;
p.vel.y = 0;
p.grounded = true;
}
// Aplicar bob encima de la altura real para dar sensación de caminar
const bob = (p.bobTime > 0) ? Math.sin(p.bobTime) * bobAmplitude : 0;
m.position.y = targetGroundY + bob;
// Si es jugador 1, mover cámara suavemente con su delta
if (isPlayer1 && idx === 0) {
// La cámara la movemos sutilmente para seguir
// (se dejó una lógica simple: la cámara se desplaza con el jugador)
// Para evitar "teletransporte", empujamos la cámara en el mismo delta que el player movió en XZ
// deltaXZ aproximado:
// No disponible aquí el oldPos, así que calculamos mínima corrección: mantener camera mirando al player
// controls.target se actualizará en animate para que la cámara siga
}
});
// Ajuste de cámara: la cámara mira siempre al player1
const p1 = players.find(x => x.id === 'player1');
if (controls && p1 && p1.mesh) {
controls.target.copy(p1.mesh.position);
}
}
// -----------------------
// Loop y utilidades
// -----------------------
function onWindowResize() {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
}
function animate() {
requestAnimationFrame(animate);
const delta = clock.getDelta();
handleWormMovement(delta);
// Actualizar controles (la target ya fue ajustada)
if (controls) controls.update();
renderer.render(scene, camera);
}
// Inicializar
window.addEventListener('DOMContentLoaded', () => {
init();
animate();
});