Meta Description" name="description" />
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">
<title>CraftSim 3D - Móvil</title>
<style>
body { margin: 0; overflow: hidden; background-color: #729FFF; font-family: sans-serif; touch-action: none; user-select: none; }
canvas { display: block; }
/* Controles Táctiles - Estilo Android */
#ui-overlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; pointer-events: none; z-index: 10; padding: env(safe-area-inset-top) env(safe-area-inset-right) env(safe-area-inset-bottom) env(safe-area-inset-left); }
.touch-zone { pointer-events: auto; background: rgba(0,0,0,0.2); color: white; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 24px; font-weight: bold; }
.touch-zone:active { background: rgba(0,0,0,0.5); }
/* Joystick (Moverse) */
#joystick-container { position: absolute; bottom: 40px; left: 40px; width: 120px; height: 120px; }
#joystick-base { position: absolute; width: 100%; height: 100%; background: rgba(0,0,0,0.2); border-radius: 50%; }
#joystick-knob { position: absolute; top: 30px; left: 30px; width: 60px; height: 60px; background: rgba(255,255,255,0.7); border-radius: 50%; pointer-events: auto; }
/* Zona de Cámara (Girar) */
#camera-zone { position: absolute; top: 0; right: 0; width: 50%; height: 70%; pointer-events: auto; background: rgba(0,0,0,0.01); }
/* Botones de Acción (Derecha) */
.action-btns { position: absolute; bottom: 40px; right: 40px; display: flex; flex-direction: column; gap: 15px; }
.btn { width: 60px; height: 60px; }
#jump-btn { background-color: rgba(255,255,255,0.5); border-radius: 10px; }
#mine-btn { background-color: rgba(200,50,50,0.5); font-size: 18px; }
#place-btn { background-color: rgba(50,200,50,0.5); font-size: 18px; }
/* Punto de Mira (Crosshair) */
#crosshair { position: fixed; top: 50%; left: 50%; width: 10px; height: 10px; background-color: rgba(255,255,255,0.7); border-radius: 50%; transform: translate(-50%, -50%); z-index: 5; }
/* Mensaje de Carga */
#loading { position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); color: white; font-size: 24px; z-index: 20; }
</style>
</head>
<body>
<div id="loading">Cargando mundo...</div>
<div id="crosshair"></div>
<div id="ui-overlay">
<div id="joystick-container">
<div id="joystick-base"></div>
<div id="joystick-knob"></div>
</div>
<div id="camera-zone"></div>
<div class="action-btns">
<div class="touch-zone btn" id="mine-btn">⛏️</div>
<div class="touch-zone btn" id="place-btn">🧱</div>
<div class="touch-zone btn" id="jump-btn">⬆️</div>
</div>
</div>
<script async src="https://unpkg.com/es-module-shims@1.8.0/dist/es-module-shims.js"></script>
<script type="importmap">
{
"imports": {
"three": "https://unpkg.com/three@0.156.1/build/three.module.js",
"three/addons/": "https://unpkg.com/three@0.156.1/examples/jsm/"
}
}
</script>
<script type="module">
import * as THREE from 'three';
// --- CONFIGURACIÓN GRÁFICA ---
const TILE_SIZE = 16; // Texturas pixeladas de 16x16
const RENDER_DISTANCE = 3; // Distancia de renderizado (menor = más rápido)
// --- VARIABLES GLOBALES ---
let scene, camera, renderer, clock, raycaster;
let player, playerVelocity, playerGrounded;
let input = { forward: 0, right: 0, jump: false, cameraDx: 0, cameraDy: 0 };
let cameraRotation = { x: 0, y: 0 };
let blocks = []; // Aquí guardaremos los cubos
const gravity = 0.5;
init();
async function init() {
// 1. Escena y Cámara
scene = new THREE.Scene();
scene.background = new THREE.Color(0x729FFF);
scene.fog = new THREE.Fog(0x729FFF, RENDER_DISTANCE * 16, RENDER_DISTANCE * 20);
camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
// 2. Renderer
renderer = new THREE.WebGLRenderer({ antialias: false }); // Desactivar antialias para el look pixelado
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
// 3. Luz
const ambientLight = new THREE.AmbientLight(0xcccccc, 1.2);
scene.add(ambientLight);
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8);
directionalLight.position.set(1, 1, 0.5).normalize();
scene.add(directionalLight);
clock = new THREE.Clock();
raycaster = new THREE.Raycaster();
// 4. Cargar Texturas y Crear Mundo
try {
const textureLoader = new THREE.TextureLoader();
// --- ¡IMPORTANTE! Reemplaza estas URLs por texturas de Minecraft reales (.png) ---
const dirtTex = textureLoader.load('https://raw.githubusercontent.com/Anand-R4/Minecraft-Clone/master/assets/dirt.png');
const grassTopTex = textureLoader.load('https://raw.githubusercontent.com/Anand-R4/Minecraft-Clone/master/assets/grass.png');
const grassSideTex = textureLoader.load('https://raw.githubusercontent.com/Anand-R4/Minecraft-Clone/master/assets/grass_side.png');
// Optimizar texturas para que sean pixeladas y se repitan bien
[dirtTex, grassTopTex, grassSideTex].forEach(tex => {
tex.magFilter = THREE.NearestFilter; // El look de píxeles grandes
tex.wrapS = THREE.RepeatWrapping;
tex.wrapT = THREE.RepeatWrapping;
});
// Definir Materiales (un cubo tiene 6 caras: x+, x-, y+, y-, z+, z-)
const matDirt = new THREE.MeshLambertMaterial({ map: dirtTex });
const matGrass = [
new THREE.MeshLambertMaterial({ map: grassSideTex }), // Right
new THREE.MeshLambertMaterial({ map: grassSideTex }), // Left
new THREE.MeshLambertMaterial({ map: grassTopTex }), // Top
new THREE.MeshLambertMaterial({ map: dirtTex }), // Bottom
new THREE.MeshLambertMaterial({ map: grassSideTex }), // Front
new THREE.MeshLambertMaterial({ map: grassSideTex }) // Back
];
const geometry = new THREE.BoxGeometry(1, 1, 1);
// Generación de Mundo Simple (Piso de 32x32)
for (let x = -16; x < 16; x++) {
for (let z = -16; z < 16; z++) {
// Capa superior (Pasto)
const blockPasto = new THREE.Mesh(geometry, matGrass);
blockPasto.position.set(x, 0, z);
scene.add(blockPasto);
blocks.push(blockPasto);
// Capa inferior (Tierra)
const blockTierra = new THREE.Mesh(geometry, matDirt);
blockTierra.position.set(x, -1, z);
scene.add(blockTierra);
blocks.push(blockTierra);
}
}
} catch (e) { console.error("Error cargando texturas:", e); }
// 5. Configurar Jugador
camera.position.set(0, 1.8, 0); // Altura de los ojos (1.8 cubos)
scene.add(camera);
player = camera;
playerVelocity = new THREE.Vector3();
playerGrounded = false;
// 6. Setup de Controles Táctiles (Joysticks y Botones)
initTouchControls();
window.addEventListener('resize', onWindowResize);
document.getElementById('loading').style.display = 'none';
animate();
}
// --- LÓGICA DE CONTROLES TÁCTILES ---
function initTouchControls() {
// A. JOYSTICK (Movimiento)
const knob = document.getElementById('joystick-knob');
const container = document.getElementById('joystick-container');
let joystickTouchId = null;
let startX, startY;
knob.addEventListener('touchstart', (e) => {
if (joystickTouchId !== null) return;
const touch = e.targetTouches[0];
joystickTouchId = touch.identifier;
const rect = container.getBoundingClientRect();
startX = touch.clientX - knob.offsetLeft - (knob.offsetWidth / 2);
startY = touch.clientY - knob.offsetTop - (knob.offsetHeight / 2);
e.stopPropagation();
});
window.addEventListener('touchmove', (e) => {
for (let i = 0; i < e.changedTouches.length; i++) {
if (e.changedTouches[i].identifier === joystickTouchId) {
const touch = e.changedTouches[i];
const rect = container.getBoundingClientRect();
let dx = touch.clientX - startX - rect.left - rect.width/2;
let dy = touch.clientY - startY - rect.top - rect.height/2;
const dist = Math.sqrt(dx*dx + dy*dy);
const maxDist = 30; // Radio máximo del joystick
if (dist > maxDist) {
dx *= maxDist / dist;
dy *= maxDist / dist;
}
knob.style.transform = `translate(${dx}px, ${dy}px)`;
// Normalizar input (-1 a 1)
input.right = dx / maxDist;
input.forward = dy / maxDist;
}
}
});
const endJoystick = (e) => {
for (let i = 0; i < e.changedTouches.length; i++) {
if (e.changedTouches[i].identifier === joystickTouchId) {
joystickTouchId = null;
knob.style.transform = `translate(0px, 0px)`;
input.forward = 0;
input.right = 0;
}
}
};
window.addEventListener('touchend', endJoystick);
window.addEventListener('touchcancel', endJoystick);
// B. ZONA DE CÁMARA (Girar)
const cameraZone = document.getElementById('camera-zone');
let cameraTouchId = null;
let prevX, prevY;
cameraZone.addEventListener('touchstart', (e) => {
const touch = e.targetTouches[0];
cameraTouchId = touch.identifier;
prevX = touch.clientX;
prevY = touch.clientY;
});
window.addEventListener('touchmove', (e) => {
for (let i = 0; i < e.changedTouches.length; i++) {
if (e.changedTouches[i].identifier === cameraTouchId) {
const touch = e.changedTouches[i];
const speed = 0.005; // Sensibilidad de cámara
input.cameraDx = (touch.clientX - prevX) * speed;
input.cameraDy = (touch.clientY - prevY) * speed;
prevX = touch.clientX;
prevY = touch.clientY;
}
}
});
const endCamera = (e) => {
for (let i = 0; i < e.changedTouches.length; i++) {
if (e.changedTouches[i].identifier === cameraTouchId) {
cameraTouchId = null;
input.cameraDx = 0;
input.cameraDy = 0;
}
}
};
window.addEventListener('touchend', endCamera);
// C. BOTONES DE ACCIÓN (Saltar, Minar, Colocar)
const jumpBtn = document.getElementById('jump-btn');
jumpBtn.addEventListener('touchstart', (e) => { e.preventDefault(); input.jump = true; });
jumpBtn.addEventListener('touchend', (e) => { e.preventDefault(); input.jump = false; });
document.getElementById('mine-btn').addEventListener('touchstart', mineBlock);
document.getElementById('place-btn').addEventListener('touchstart', placeBlock);
}
// --- FÍSICAS Y ACTUALIZACIÓN ---
function animate() {
requestAnimationFrame(animate);
const delta = clock.getDelta();
// 1. Girar Cámara
if (cameraTouchId !== null) {
cameraRotation.y -= input.cameraDx;
cameraRotation.x -= input.cameraDy;
cameraRotation.x = Math.max(-Math.PI/2, Math.min(Math.PI/2, cameraRotation.x)); // Limitar mirar arriba/abajo
camera.rotation.set(cameraRotation.x, cameraRotation.y, 0, 'YXZ'); // Orden de rotación muy importante
}
// 2. Movimiento y Gravedad
const moveSpeed = 6 * delta;
const jumpPower = 10;
// Calcular vectores forward/right relativos a la cámara
const forwardVector = new THREE.Vector3(0, 0, -1).applyQuaternion(camera.quaternion);
const rightVector = new THREE.Vector3(1, 0, 0).applyQuaternion(camera.quaternion);
// Aplanar vectores (no volar si miras arriba)
forwardVector.y = 0; forwardVector.normalize();
rightVector.y = 0; rightVector.normalize();
// Aplicar Input
const moveVector = new THREE.Vector3();
moveVector.addScaledVector(forwardVector, -input.forward);
moveVector.addScaledVector(rightVector, input.right);
moveVector.normalize().multiplyScalar(moveSpeed);
playerVelocity.x = moveVector.x;
playerVelocity.z = moveVector.z;
playerVelocity.y -= gravity * delta * jumpPower; // Gravedad
// Saltar
if (playerGrounded && input.jump) {
playerVelocity.y = jumpPower * delta;
playerGrounded = false;
}
player.position.add(playerVelocity);
// Colisión Simple con el Piso (y = 1.8)
if (player.position.y < 1.8) {
playerVelocity.y = 0;
player.position.y = 1.8;
playerGrounded = true;
}
renderer.render(scene, camera);
}
// --- ACCIONES DE BLOQUES (Minar/Colocar) ---
function mineBlock(e) {
if (e) e.preventDefault();
// Disparamos un rayo desde el centro de la pantalla
raycaster.setFromCamera(new THREE.Vector2(), camera);
raycaster.far = 4; // Distancia máxima de minado
const intersects = raycaster.intersectObjects(blocks);
if (intersects.length > 0) {
const block = intersects[0].object;
scene.remove(block); // Quitar de la escena
blocks.splice(blocks.indexOf(block), 1); // Quitar de la lista
}
}
function placeBlock(e) {
if (e) e.preventDefault();
raycaster.setFromCamera(new THREE.Vector2(), camera);
raycaster.far = 4;
const intersects = raycaster.intersectObjects(blocks);
if (intersects.length > 0) {
const intersect = intersects[0];
const blockNormal = intersect.face.normal; // La cara donde tocamos (arriba, abajo, lado)
const blockPos = intersect.object.position;
// Calcular la posición del nuevo bloque sumando la normal
const newPos = blockPos.clone().add(blockNormal);
// Crear nuevo bloque (copiando el material de pasto)
const matGrassCopy = intersect.object.material.slice ? intersect.object.material : [intersect.object.material];
const newBlock = new THREE.Mesh(new THREE.BoxGeometry(1, 1, 1), matGrassCopy);
newBlock.position.copy(newPos);
scene.add(newBlock);
blocks.push(newBlock);
}
}
function onWindowResize() {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
}
</script>
</body>
</html>7
4
274KB
1269KB
380.0ms
200.0ms
578.0ms