🎮ArcadeLab

Block World 3D

by MagicKnight13
431 lines18.0 KB🛠️ Three.js (3D graphics)
▶ Play

<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
<title>Block World 3D</title>
<style>
*{box-sizing:border-box}
body{margin:0;background:#253b2a;color:white;font-family:Arial,sans-serif}
button,input{font:inherit}
button{background:#4b913b;color:white;border:2px solid #b5e58c;padding:11px;
margin:4px;cursor:pointer;font-weight:bold;border-radius:3px;touch-action:manipulation}
button:active{filter:brightness(1.3)}
input{padding:12px;width:100%;margin:8px 0}
header{text-align:center;padding:14px;background:#263e2a}
h1{margin:0;color:#b8f18d}
main{max-width:850px;margin:auto;padding:12px}
.panel{background:#293d32;border:2px solid #678b62;border-radius:5px;padding:14px;margin:10px 0}
.hidden{display:none!important}
.world{display:flex;justify-content:space-between;align-items:center;gap:6px;
flex-wrap:wrap;background:#3b5143;padding:10px;margin:8px 0}
.modes{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px}
.mode{background:#3a5142;padding:10px;border:2px solid #71956b}
.mode button{width:100%;margin:3px 0}
.mode p,small{color:#d4e1d1;font-size:13px}
#game{height:75vh;height:75dvh;min-height:350px;position:relative;overflow:hidden;
background:#80c9f2;touch-action:none}
#hud{position:absolute;inset:0;pointer-events:none;color:white;text-shadow:1px 1px #222}
#top{position:absolute;top:6px;left:6px;right:6px;display:flex;
justify-content:space-between;align-items:start;gap:4px}
#top button{font-size:11px;padding:8px 5px}
#crosshair{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);font-size:24px}
#toast{position:absolute;top:57px;left:7px;background:#17251ddd;padding:7px;font-size:12px}
#bottom{position:absolute;bottom:5px;left:5px;right:5px;display:flex;
justify-content:space-between;align-items:end;gap:3px}
#move{display:grid;grid-template-columns:repeat(3,38px);gap:2px;pointer-events:auto}
#move button{margin:0;padding:8px 0;background:#263b49df}
.up{grid-column:2}.left{grid-column:1;grid-row:2}
.down{grid-column:2;grid-row:2}.right{grid-column:3;grid-row:2}
#actions{display:flex;flex-direction:column;pointer-events:auto}
#actions button{margin:2px;padding:8px 4px;font-size:10px;background:#263b49ed}
#hotbar{display:flex;justify-content:center;flex-wrap:wrap;max-width:180px;pointer-events:auto}
.slot{width:39px;height:42px;padding:2px;margin:1px;font-size:10px;background:#343434dd}
.slot.selected{border:3px solid yellow}
#inventory{display:none;position:absolute;bottom:75px;left:4%;right:4%;
background:#1c2a27f5;padding:10px;text-align:center;pointer-events:auto}
#inventory button{font-size:12px}
#loading{display:none;position:fixed;inset:0;background:#17251cf5;z-index:20;
align-items:center;justify-content:center;text-align:center;padding:20px}
#bar{height:18px;background:#111;border:2px solid #b8ef90;width:min(330px,80vw);margin:18px auto}
#progress{height:100%;width:0;background:#71ce4c}
@media(max-width:430px){main{padding:6px}.panel{padding:10px}
#hotbar{max-width:130px}.slot{width:35px;font-size:9px}}
</style>
</head>
<body>
<header><h1>⛏ BLOCK WORLD 3D</h1><small>Explora y construye</small></header>

<main id="menu">
<section id="home" class="panel">
<h2>Menú principal</h2>
<button style="width:100%" onclick="showScreen('worlds')">▶ JUGAR</button>
<button style="width:100%" onclick="showScreen('market')">🛒 MARKETPLACE</button>
<button style="width:100%" onclick="showScreen('settings')">⚙ AJUSTES</button>
</section>

<section id="worlds" class="panel hidden">
<h2>Mis mundos</h2><div id="worldList"></div>
<button onclick="showScreen('create')">+ CREAR MUNDO</button>
<button onclick="showScreen('home')">VOLVER</button>
</section>

<section id="create" class="panel hidden">
<h2>Crear mundo</h2>
<input id="worldName" maxlength="40" placeholder="Nombre de tu mundo">
<button style="width:100%" onclick="createWorld()">CREAR Y CONTINUAR</button>
<button onclick="showScreen('worlds')">CANCELAR</button>
</section>

<section id="chooseMode" class="panel hidden">
<h2>Elegir modo de juego</h2><p id="selectedWorldName"></p>
<div class="modes">
<div class="mode"><h3>🌲 Supervivencia</h3><p>Recoge bloques y explora.</p>
<button onclick="startSelectedWorld('survival')">ELEGIR</button></div>
<div class="mode"><h3>🧱 Creativo</h3><p>Construye libremente.</p>
<button onclick="startSelectedWorld('creative')">ELEGIR</button></div>
<div class="mode"><h3>🗺 Aventura</h3><p>Explora sin colocar ni romper.</p>
<button onclick="startSelectedWorld('adventure')">ELEGIR</button></div>
<div class="mode"><h3>💀 Hardcore</h3><p>Supervivencia con una vida.</p>
<button onclick="startSelectedWorld('hardcore')">ELEGIR</button></div>
</div>
<button onclick="showScreen('worlds')">VOLVER</button>
</section>

<section id="market" class="panel hidden">
<h2>Marketplace</h2><div id="addonList"></div>
<button onclick="showScreen('home')">VOLVER</button>
</section>

<section id="settings" class="panel hidden">
<h2>Ajustes</h2>
<label><input id="touchLook" type="checkbox" checked style="width:auto">
Girar la cámara tocando y arrastrando</label>
<button onclick="showScreen('home')">VOLVER</button>
</section>
</main>

<section id="gameScreen" class="hidden">
<div id="game"></div>
<div id="hud">
<div id="top">
<div id="modeStatus" class="panel">Mi mundo</div>
<div><button id="cameraBtn">TERCERA PERSONA</button>
<button id="pauseBtn">PAUSA</button></div>
</div>
<div id="crosshair">+</div><div id="toast">¡Bienvenido!</div>
<div id="bottom">
<div id="move">
<button class="up" data-move="forward">▲</button>
<button class="left" data-move="left">◀</button>
<button class="down" data-move="back">▼</button>
<button class="right" data-move="right">▶</button>
</div>
<div id="hotbar"></div>
<div id="actions">
<button id="jumpBtn">SALTAR</button>
<button id="breakBtn">ROMPER</button>
<button id="placeBtn">COLOCAR</button>
<button id="invBtn">MOCHILA</button>
</div>
</div>
<div id="inventory" class="panel"><h3>Inventario</h3>
<div id="inventoryItems"></div><button id="closeInv">CERRAR</button></div>
</div>
</section>

<div id="loading"><div><h2 id="loadingTitle">Generando mundo...</h2>
<p id="loadingText">Preparando terreno</p>
<div id="bar"><div id="progress"></div></div></div></div>

<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
<script>
(function(){
"use strict";
const $=id=>document.getElementById(id);
const screenNames=["home","worlds","create","chooseMode","market","settings"];
let worlds=[];
try{worlds=JSON.parse(localStorage.getItem("bw_worlds")||"[]")}catch(e){worlds=[]}
let addons={};
try{addons=JSON.parse(localStorage.getItem("bw_addons")||"{}")}catch(e){addons={}}
const addonData=[
{id:"verity",name:"Verity",desc:"Esfera amarilla de demostración."},
{id:"huggy",name:"Huggy & sus amigos",desc:"Criaturas de demostración."},
{id:"actions",name:"Actions & Stuff",desc:"Vista previa visual."},
{id:"ruby",name:"Ruby Update",desc:"Bloques y objetos de rubí."}
];
let pendingWorld=null,currentWorld=null,mode="creative";
let cameraMode="first",playing=false,paused=false,dead=false;
let scene,camera,renderer,geometry,materials;
let blocks=new Map(),meshes=[];
let player={x:0,y:5,z:0,vy:0,grounded:false};
let yaw=0,pitch=0,lastTime=0,dragging=false,lastX=0,lastY=0;
const keys={},mobile={};
const types=["grass","dirt","stone","wood","sand","leaves","ruby"];
const names={grass:"Hierba",dirt:"Tierra",stone:"Piedra",wood:"Madera",
sand:"Arena",leaves:"Hojas",ruby:"Rubí"};
let inventory={grass:32,dirt:32,stone:24,wood:16,sand:16,leaves:16,ruby:0};
let selected="grass";
const raycaster=new THREE.Raycaster();

function save(){
try{
localStorage.setItem("bw_worlds",JSON.stringify(worlds));
localStorage.setItem("bw_addons",JSON.stringify(addons));
}catch(e){}
}
window.showScreen=function(id){
screenNames.forEach(s=>$(s).classList.toggle("hidden",s!==id));
$("menu").classList.remove("hidden");
$("gameScreen").classList.add("hidden");
if(id==="worlds")renderWorlds();
if(id==="market")renderMarket();
};
function renderWorlds(){
const list=$("worldList");list.innerHTML="";
if(!worlds.length)list.innerHTML="<p>No hay mundos todavía. Crea uno.</p>";
worlds.forEach(w=>{
const row=document.createElement("div");row.className="world";
const name=document.createElement("span");name.textContent="🌎 "+w.name;
const buttons=document.createElement("div");
const play=document.createElement("button");play.textContent="JUGAR";
play.onclick=()=>{pendingWorld=w;showModes();};
const del=document.createElement("button");del.textContent="ELIMINAR";
del.onclick=()=>{
if(confirm("¿Eliminar "+w.name+"?")){
worlds=worlds.filter(v=>v.id!==w.id);save();renderWorlds();
}};
buttons.append(play,del);row.append(name,buttons);list.append(row);
});
}
window.createWorld=function(){
const name=$("worldName").value.trim()||"Mundo nuevo";
pendingWorld={id:Date.now(),name:name,mode:"creative"};
worlds.push(pendingWorld);save();$("worldName").value="";
showModes();
};
function showModes(){
screenNames.forEach(s=>$(s).classList.toggle("hidden",s!=="chooseMode"));
$("selectedWorldName").textContent="Mundo: "+pendingWorld.name;
}
window.startSelectedWorld=function(chosen){
mode=chosen;currentWorld=pendingWorld;currentWorld.mode=chosen;
const w=worlds.find(v=>v.id===currentWorld.id);if(w)w.mode=chosen;
save();loadWorld();
};
function loadWorld(){
$("menu").classList.add("hidden");$("gameScreen").classList.add("hidden");
$("loading").style.display="flex";$("progress").style.width="0%";
$("loadingTitle").textContent="Generando "+currentWorld.name;
let n=0;const labels=["Preparando terreno...","Creando árboles...",
"Preparando bloques...","Configurando modo...","¡Listo!"];
const timer=setInterval(()=>{
n++;$("progress").style.width=(n/labels.length*100)+"%";
$("loadingText").textContent=labels[n-1];
if(n>=labels.length){clearInterval(timer);$("loading").style.display="none";enterGame();}
},220);
}
function modeName(m){return {creative:"Creativo",survival:"Supervivencia",
adventure:"Aventura",hardcore:"Hardcore"}[m]||m;}
function setup3D(){
scene=new THREE.Scene();scene.background=new THREE.Color(0x80c9f2);
scene.fog=new THREE.Fog(0x80c9f2,22,75);
camera=new THREE.PerspectiveCamera(75,1,.1,1000);
renderer=new THREE.WebGLRenderer({antialias:false});
renderer.setPixelRatio(Math.min(devicePixelRatio,1.5));
$("game").appendChild(renderer.domElement);
scene.add(new THREE.HemisphereLight(0xffffff,0x63764d,2));
const sun=new THREE.DirectionalLight(0xffffff,1.4);sun.position.set(15,30,10);scene.add(sun);
geometry=new THREE.BoxGeometry(1,1,1);
materials={};
const colors={grass:0x60a83d,dirt:0x80502f,stone:0x858585,
wood:0x986332,sand:0xd7c27a,leaves:0x317d36,ruby:0xc52248};
Object.keys(colors).forEach(k=>materials[k]=new THREE.MeshLambertMaterial({color:colors[k]}));
renderer.domElement.addEventListener("pointerdown",onPointerDown);
renderer.domElement.addEventListener("pointermove",onPointerMove);
renderer.domElement.addEventListener("pointerup",()=>dragging=false);
renderer.domElement.addEventListener("pointercancel",()=>dragging=false);
renderer.domElement.addEventListener("contextmenu",e=>e.preventDefault());
resize3D();
}
function resize3D(){
if(!renderer||!camera)return;
const w=$("game").clientWidth,h=$("game").clientHeight;
camera.aspect=w/Math.max(h,1);camera.updateProjectionMatrix();renderer.setSize(w,h);
}
function addBlock(x,y,z,type){
x=Math.round(x);y=Math.round(y);z=Math.round(z);
const k=x+","+y+","+z;if(blocks.has(k))return;
const mesh=new THREE.Mesh(geometry,materials[type]||materials.grass);
mesh.position.set(x,y,z);mesh.userData={x,y,z,type};
scene.add(mesh);blocks.set(k,mesh);meshes.push(mesh);
}
function removeBlock(mesh){
if(!mesh)return;
scene.remove(mesh);const d=mesh.userData;
blocks.delete(d.x+","+d.y+","+d.z);
const i=meshes.indexOf(mesh);if(i>=0)meshes.splice(i,1);
}
function generateWorld(){
for(let x=-12;x<=12;x++)for(let z=-12;z<=12;z++){
const h=Math.max(1,Math.floor(2+Math.sin(x*.45)*Math.cos(z*.37)*1.5));
for(let y=0;y<h;y++)addBlock(x,y,z,y===h-1?"grass":"dirt");
if(Math.random()<.025){
for(let y=h;y<h+3;y++)addBlock(x,y,z,"wood");
for(let dx=-1;dx<=1;dx++)for(let dz=-1;dz<=1;dz++)
for(let dy=2;dy<=4;dy++)if(Math.abs(dx)+Math.abs(dz)+(dy===4?1:0)<4)
addBlock(x+dx,h+dy,z+dz,"leaves");
}
if(addons.ruby&&Math.random()<.1)addBlock(x,h-1,z,"ruby");
}
}
function clearWorld(){
for(const m of meshes)scene.remove(m);
meshes=[];blocks.clear();
}
function enterGame(){
if(!window.THREE){alert("Conéctate a internet para cargar el motor 3D.");showScreen("home");return;}
if(!renderer)setup3D();
clearWorld();generateWorld();
player={x:0,y:5,z:0,vy:0,grounded:false};
yaw=0;pitch=0;paused=false;dead=false;playing=true;
$("menu").classList.add("hidden");$("gameScreen").classList.remove("hidden");
$("modeStatus").textContent=currentWorld.name+" · "+modeName(mode);
$("toast").textContent=mode==="hardcore"?"HARDCORE: una sola vida":"Modo "+modeName(mode);
$("inventory").style.display="none";
renderHotbar();renderInventory();updateCamera();resize3D();
}
function target(){
raycaster.setFromCamera(new THREE.Vector2(0,0),camera);
return raycaster.intersectObjects(meshes,false).find(h=>h.distance<7);
}
function breakBlock(){
if(!playing||paused||dead)return;
if(mode==="adventure"){toast("En Aventura no puedes romper bloques.");return;}
const hit=target();if(!hit)return;
const type=hit.object.userData.type;removeBlock(hit.object);
if(mode!=="creative")inventory[type]=(inventory[type]||0)+1;
renderHotbar();renderInventory();
}
function placeBlock(){
if(!playing||paused||dead)return;
if(mode==="adventure"){toast("En Aventura no puedes colocar bloques.");return;}
if(mode!=="creative"&&(inventory[selected]||0)<=0){toast("No tienes ese bloque.");return;}
const hit=target();if(!hit)return;
const p=hit.object.position.clone().add(hit.face.normal);
const x=Math.round(p.x),y=Math.round(p.y),z=Math.round(p.z);
if(Math.abs(x-player.x)<1&&Math.abs(z-player.z)<1&&y>=player.y-1&&y<=player.y+2)return;
addBlock(x,y,z,selected);
if(mode!=="creative")inventory[selected]--;
renderHotbar();renderInventory();
}
function renderHotbar(){
const bar=$("hotbar");bar.innerHTML="";
types.slice(0,5).forEach(type=>{
const b=document.createElement("button");b.className="slot"+(selected===type?" selected":"");
b.textContent=names[type]+(mode==="creative"?"":" "+(inventory[type]||0));
b.onclick=()=>{selected=type;renderHotbar();};bar.append(b);
});
}
function renderInventory(){
const el=$("inventoryItems");el.innerHTML="";
types.forEach(type=>{
const b=document.createElement("button");
b.textContent=names[type]+" "+(mode==="creative"?"∞":inventory[type]||0);
b.onclick=()=>{selected=type;renderHotbar();};el.append(b);
});
}
function toast(text){$("toast").textContent=text;}
$("breakBtn").onclick=breakBlock;
$("placeBtn").onclick=placeBlock;
$("jumpBtn").onclick=jump;
$("invBtn").onclick=()=>{
$("inventory").style.display=$("inventory").style.display==="block"?"none":"block";
renderInventory();
};
$("closeInv").onclick=()=>$("inventory").style.display="none";
$("cameraBtn").onclick=()=>{
cameraMode=cameraMode==="first"?"third":"first";
$("cameraBtn").textContent=cameraMode==="first"?"TERCERA PERSONA":"PRIMERA PERSONA";
updateCamera();
};
$("pauseBtn").onclick=()=>{
paused=!paused;$("pauseBtn").textContent=paused?"CONTINUAR":"PAUSA";
toast(paused?"Partida pausada":"Partida reanudada");
};
function jump(){if(playing&&!paused&&!dead&&player.grounded){player.vy=7;player.grounded=false;}}
document.addEventListener("keydown",e=>{
keys[e.key.toLowerCase()]=true;
if([" ","arrowup","arrowdown"].includes(e.key.toLowerCase()))e.preventDefault();
if(e.key>="1"&&e.key<="5"){selected=types[Number(e.key)-1];renderHotbar();}
});
document.addEventListener("keyup",e=>keys[e.key.toLowerCase()]=false);
document.querySelectorAll("[data-move]").forEach(b=>{
const d=b.dataset.move;
const down=e=>{e.preventDefault();mobile[d]=true;};
const up=e=>{e.preventDefault();mobile[d]=false;};
b.addEventListener("pointerdown",down);b.addEventListener("pointerup",up);
b.addEventListener("pointercancel",up);b.addEventListener("pointerleave",up);
});
function onPointerDown(e){
if(!playing||paused)return;
if(e.pointerType==="mouse"){if(e.button===0)breakBlock();if(e.button===2)placeBlock();}
if(e.pointerType==="touch"&&$("touchLook").checked){
dragging=true;lastX=e.clientX;lastY=e.clientY;
}
}
function onPointerMove(e){
if(!dragging||!playing||paused||!$("touchLook").checked)return;
yaw-=(e.clientX-lastX)*.006;pitch-=(e.clientY-lastY)*.006;
pitch=Math.max(-1.2,Math.min(1.2,pitch));lastX=e.clientX;lastY=e.clientY;
}
function updateCamera(){
if(!camera)return;
if(cameraMode==="first"){
camera.position.set(player.x,player.y+1.6,player.z);
camera.rotation.set(pitch,yaw,0,"YXZ");
}else{
camera.position.set(player.x+Math.sin(yaw)*5,player.y+2.5,player.z+Math.cos(yaw)*5);
camera.lookAt(player.x,player.y+1,player.z);
}
}
function update(now){
requestAnimationFrame(update);
const dt=Math.min((now-lastTime)/1000,.04)||0;lastTime=now;
if(renderer&&scene&&playing&&!paused&&!dead){
let f=0,s=0;
if(keys.w||keys.arrowup||mobile.forward)f++;
if(keys.s||keys.arrowdown||mobile.back)f--;
if(keys.a||mobile.left)s--;
if(keys.d||mobile.right)s++;
const len=Math.hypot(f,s)||1;f/=len;s/=len;
const speed=mode==="creative"?7:4.5;
player.x+=(-Math.sin(yaw)*f+Math.cos(yaw)*s)*speed*dt;
player.z+=(-Math.cos(yaw)*f-Math.sin(yaw)*s)*speed*dt;
if(keys[" "]&&player.grounded)jump();
player.vy-=18*dt;player.y+=player.vy*dt;
if(player.y<3){player.y=3;player.vy=0;player.grounded=true;}
updateCamera();
}
if(renderer&&scene&&camera)renderer.render(scene,camera);
}
function renderMarket(){
const list=$("addonList");list.innerHTML="";
addonData.forEach(a=>{
const card=document.createElement("div");card.className="panel";
const title=document.createElement("h3");title.textContent=a.name;
const desc=document.createElement("p");desc.textContent=a.desc;
const status=document.createElement("p");status.textContent=addons[a.id]?"Descargado y activado":"No activado";
const btn=document.createElement("button");btn.textContent=addons[a.id]?"DESACTIVAR":"DESCARGAR / ACTIVAR";
btn.onclick=()=>{addons[a.id]=!addons[a.id];save();renderMarket();};
card.append(title,desc,status,btn);list.append(card);
});
}
addEventListener("resize",resize3D);
renderWorlds();showScreen("home");requestAnimationFrame(update);
})();
</script>
</body>
</html>

Game Source: Block World 3D

Creator: MagicKnight13

Libraries: three

Complexity: complex (431 lines, 18.0 KB)

The full source code is displayed above on this page.

Remix Instructions

To remix this game, copy the source code above and modify it. Add a ARCADELAB header at the top with "remix_of: block-world-3d-magicknight13" to link back to the original. Then publish at arcadelab.ai/publish.