Craft 3D
by MagicKnight13480 lines21.4 KB🛠️ Three.js (3D graphics)
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no">
<title>Craft 3D</title>
<style>
*{box-sizing:border-box}
body{margin:0;background:#17251b;color:white;font-family:system-ui,Arial}
button,input,select{font:inherit}
button{background:#80c94f;color:#142113;border:2px solid #111b12;border-radius:7px;padding:10px;font-weight:bold;cursor:pointer}
button:active{transform:scale(.97)}
.secondary{background:#304633;color:white}
.hidden{display:none!important}
.screen{min-height:100vh;padding:16px;display:flex;justify-content:center;align-items:center}
.panel{width:min(850px,100%);background:#25382a;border:3px solid #101a12;border-radius:12px;padding:20px}
h1{text-align:center;font-size:clamp(42px,10vw,70px);margin:0}
h2{margin-top:0}
p{color:#d0dbc9}
.menu{display:grid;grid-template-columns:1fr 1fr;gap:10px}
.menu button:first-child{grid-column:1/-1;font-size:22px}
.row{display:flex;justify-content:space-between;align-items:center;gap:8px;flex-wrap:wrap}
.field{margin:14px 0}
.field label{display:block;margin-bottom:6px}
.field input,.field select{width:100%;padding:10px;background:#142118;color:white;border:1px solid #72876b;border-radius:6px}
.cards{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:10px;margin:14px 0}
.card{padding:12px;background:#17271c;border:2px solid #50664a;border-radius:9px;min-width:0}
.art{text-align:center;font-size:48px}
.card button{width:100%;margin-top:6px}
.world{padding:12px;margin:10px 0;background:#17271c;border:1px solid #50664a;border-radius:8px}
.check{display:flex;gap:10px;align-items:center;padding:9px 0}
.check input{width:20px;height:20px}
#game{position:fixed;inset:0;overflow:hidden;background:#83c9ef}
#view{width:100%;height:100%;display:block}
.hud{position:absolute;inset:8px;pointer-events:none;display:flex;flex-direction:column;justify-content:space-between}
.hud button,.hud span,.hud .controls{pointer-events:auto}
.hudtop{display:flex;justify-content:space-between;align-items:center;gap:6px;flex-wrap:wrap}
.hudtop span{background:#15251bd9;padding:8px;border-radius:6px}
.controls{display:flex;justify-content:space-between;align-items:end;gap:8px}
.pad,.actions{display:grid;grid-template-columns:repeat(3,44px);gap:4px}
.pad button,.actions button{padding:10px 2px;min-height:42px}
.actions{grid-template-columns:repeat(2,auto)}
.hotbar{display:flex;justify-content:center;gap:4px;margin-bottom:8px}
.slot{width:39px;height:39px;background:#17251bcc;border:2px solid #aaa;display:grid;place-items:center}
#pause{position:fixed;inset:0;background:#101a12dc;z-index:5}
#pause .panel{max-width:420px}
#pause button{width:100%;margin:5px 0}
#toast{position:fixed;top:10px;left:50%;transform:translateX(-50%);background:#17251b;padding:10px;border-radius:8px;z-index:10}
@media(max-width:600px){
.cards{grid-template-columns:1fr}
.card{display:grid;grid-template-columns:65px 1fr;column-gap:10px}
.card .art{grid-row:span 3}
.card button,.card .status{grid-column:2}
.panel{padding:14px}
}
</style>
</head>
<body>
<section id="main" class="screen">
<div class="panel">
<h1>CRAFT</h1>
<p style="text-align:center">Explora. Construye. Crea.</p>
<div class="menu">
<button onclick="openWorlds()">▶ JUGAR</button>
<button class="secondary" onclick="show('realms')">REALMS</button>
<button class="secondary" onclick="show('settings')">AJUSTES</button>
<button class="secondary" onclick="openMarket()">MARKETPLACE</button>
<button class="secondary" onclick="show('skin')">VESTIDOR</button>
</div>
</div>
</section>
<section id="loading" class="screen hidden">
<div class="panel">
<h1>CRAFT</h1>
<h2 id="loadtitle">Cargando...</h2>
<div style="height:18px;background:#101a12;border:1px solid #aaa;margin:20px 0">
<div id="loadbar" style="height:100%;width:0;background:#80c94f"></div>
</div>
<p id="loadpercent">0%</p>
</div>
</section>
<section id="worlds" class="screen hidden">
<div class="panel">
<div class="row"><h2>Mis mundos</h2><button class="secondary" onclick="show('main')">Menú</button></div>
<div class="row">
<button onclick="quick('Creativo')">+ CREAR CREATIVO</button>
<button onclick="quick('Supervivencia')">+ CREAR SUPERVIVENCIA</button>
</div>
<div id="worldlist"></div>
<p id="empty">Todavía no tienes mundos.</p>
<button onclick="show('create')">CREAR MUNDO PERSONALIZADO</button>
</div>
</section>
<section id="create" class="screen hidden">
<div class="panel">
<h2>Crear mundo</h2>
<div class="field"><label>Nombre</label><input id="wname" value="Mi mundo" maxlength="32"></div>
<div class="field"><label>Modo</label><select id="wmode"><option>Supervivencia</option><option>Creativo</option></select></div>
<button onclick="show('worldaddons')">COMPLEMENTOS DEL MUNDO</button>
<p id="chosenSummary">Ningún complemento seleccionado.</p>
<button onclick="createWorld()">CREAR Y JUGAR</button>
<button class="secondary" onclick="openWorlds()">Volver</button>
</div>
</section>
<section id="worldaddons" class="screen hidden">
<div class="panel">
<h2>Complementos de este mundo</h2>
<p>Primero descarga los complementos en Marketplace. Después, marca cuáles quieres utilizar en este mundo.</p>
<div id="worldAddonList"></div>
<button onclick="saveWorldAddonChoice()">GUARDAR SELECCIÓN</button>
<button class="secondary" onclick="show('create')">Volver</button>
</div>
</section>
<section id="market" class="screen hidden">
<div class="panel">
<div class="row"><h2>Marketplace</h2><button class="secondary" onclick="show('main')">Volver</button></div>
<p>Descarga los complementos para agregarlos a tu biblioteca.</p>
<div id="marketcards" class="cards"></div>
<p>Los complementos descargados se pueden seleccionar para cada mundo.</p>
</div>
</section>
<section id="settings" class="screen hidden">
<div class="panel"><h2>Ajustes</h2>
<div class="field"><label>Velocidad de movimiento</label><input id="speed" type="range" min="2" max="12" value="6"></div>
<button onclick="show('main')">Guardar y volver</button></div>
</section>
<section id="skin" class="screen hidden">
<div class="panel"><h2>Vestidor</h2><p>🧑🏻🌾 Personaje de prueba</p><button onclick="show('main')">Volver</button></div>
</section>
<section id="realms" class="screen hidden">
<div class="panel"><h2>Realms</h2><p>El juego en línea aún no está implementado.</p><button onclick="show('main')">Volver</button></div>
</section>
<section id="game" class="hidden">
<canvas id="view"></canvas>
<div class="hud">
<div class="hudtop">
<button onclick="pauseGame()">Ⅱ PAUSAR</button>
<span id="worldlabel">Mi mundo</span>
<button onclick="toast('Progreso guardado en esta sesión')">GUARDAR</button>
</div>
<div>
<div class="controls">
<div class="pad">
<span></span><button data-move="forward">▲</button><span></span>
<button data-move="left">◀</button><button data-move="back">▼</button><button data-move="right">▶</button>
</div>
<div class="actions">
<button onclick="jump()">SALTAR</button>
<button onclick="placeBlock()">PONER</button>
<button onclick="breakBlock()">ROMPER</button>
</div>
</div>
<div class="hotbar"><div class="slot">🟩</div><div class="slot">🟫</div><div class="slot">🪨</div><div class="slot">🪵</div><div class="slot">🌼</div></div>
</div>
</div>
</section>
<section id="pause" class="screen hidden">
<div class="panel">
<h2>Juego en pausa</h2>
<button onclick="resumeGame()">CONTINUAR</button>
<button class="secondary" onclick="showInventory()">INVENTARIO</button>
<button class="secondary" onclick="openWorldAddonsForCurrent()">COMPLEMENTOS DEL MUNDO</button>
<button onclick="exitWorld()">GUARDAR Y SALIR</button>
</div>
</section>
<section id="inventory" class="screen hidden">
<div class="panel"><h2>Inventario</h2><p>🟩 Césped ×64 · 🪵 Madera ×32 · 🪨 Piedra ×64</p><button onclick="resumeGame()">Volver al mundo</button></div>
</section>
<div id="toast" class="hidden"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
<script>
const ids=['main','loading','worlds','create','worldaddons','market','settings','skin','realms','game','pause','inventory'];
const definitions={
verity:{name:'Verity',icon:'🟡',description:'Un compañero esférico amarillo en tu mundo 3D.'},
huggy:{name:'Huggy & sus amigos',icon:'🧸',description:'Añade una criatura azul de prueba al mundo.'},
actions:{name:'Actions & Stuff',icon:'🧱',description:'Cambia el aspecto visual de los bloques.'}
};
let downloaded=read('craft_downloaded',[]);
let enabled=read('craft_enabled',{});
let worlds=read('craft_worlds',[]);
let selected=[];
let current=null;
let renderer,scene,camera,clock;
let player,verityObject,huggyObject;
let blocks=[];
let keys={};
let moveState={forward:false,back:false,left:false,right:false};
let yaw=0,pitch=0,velocityY=0;
let running=false;
let lastTime=0;
function read(key,fallback){
try{return JSON.parse(localStorage.getItem(key))??fallback}catch(e){return fallback}
}
function write(key,value){
try{localStorage.setItem(key,JSON.stringify(value));return true}catch(e){return false}
}
function show(id){
ids.forEach(s=>document.getElementById(s).classList.toggle('hidden',s!==id));
if(id!=='game')running=false;
}
function toast(s){
const t=document.getElementById('toast');t.textContent=s;t.classList.remove('hidden');
setTimeout(()=>t.classList.add('hidden'),1800);
}
function startLoading(title,done){
show('loading');document.getElementById('loadtitle').textContent=title;
let p=0;document.getElementById('loadbar').style.width='0%';
const timer=setInterval(()=>{
p=Math.min(100,p+10);
document.getElementById('loadbar').style.width=p+'%';
document.getElementById('loadpercent').textContent=p+'%';
if(p===100){clearInterval(timer);done()}
},90);
}
function openWorlds(){
startLoading('Abriendo mundos...',()=>{renderWorlds();show('worlds')});
}
function renderWorlds(){
const list=document.getElementById('worldlist');list.replaceChildren();
document.getElementById('empty').classList.toggle('hidden',worlds.length>0);
worlds.forEach(w=>{
const box=document.createElement('div');box.className='world';
const info=document.createElement('div');
info.innerHTML='<strong></strong><br><small></small>';
info.querySelector('strong').textContent=w.name;
info.querySelector('small').textContent=w.mode+' · Complementos: '+(w.addons||[]).length;
const play=document.createElement('button');play.textContent='JUGAR';play.onclick=()=>loadWorld(w.id);
const addons=document.createElement('button');addons.className='secondary';addons.textContent='COMPLEMENTOS';
addons.onclick=()=>{current=w;selected=[...(w.addons||[])];openWorldAddonsForCurrent()};
const del=document.createElement('button');del.className='secondary';del.textContent='ELIMINAR';
del.onclick=()=>{worlds=worlds.filter(x=>x.id!==w.id);write('craft_worlds',worlds);renderWorlds()};
box.append(info,play,addons,del);list.append(box);
});
}
function quick(mode){
document.getElementById('wname').value='Mi mundo';
document.getElementById('wmode').value=mode;
selected=[];
updateSummary();show('create');
}
function createWorld(){
const name=document.getElementById('wname').value.trim()||'Mi mundo';
const mode=document.getElementById('wmode').value;
const w={id:Date.now().toString(36)+Math.random().toString(36).slice(2),name,mode,addons:[...selected]};
worlds.push(w);
if(!write('craft_worlds',worlds))toast('No se pudo guardar permanentemente; el mundo se conserva en esta sesión.');
current=w;
startLoading('Generando mundo 3D...',()=>startGame(w));
}
function loadWorld(id){
current=worlds.find(w=>w.id===id);if(!current)return;
startLoading('Cargando mundo...',()=>startGame(current));
}
function openMarket(){renderMarket();show('market')}
function renderMarket(){
const cards=document.getElementById('marketcards');cards.replaceChildren();
Object.entries(definitions).forEach(([id,a])=>{
const card=document.createElement('article');card.className='card';
const art=document.createElement('div');art.className='art';art.textContent=a.icon;
const title=document.createElement('h3');title.textContent=a.name;
const desc=document.createElement('p');desc.textContent=a.description;
const status=document.createElement('div');status.className='status';
status.textContent=downloaded.includes(id)?'Descargado':'No descargado';
const download=document.createElement('button');
download.textContent=downloaded.includes(id)?'DESCARGADO ✓':'DESCARGAR';
download.disabled=downloaded.includes(id);
download.onclick=()=>{
if(!downloaded.includes(id))downloaded.push(id);
write('craft_downloaded',downloaded);renderMarket();toast(a.name+' agregado a tu biblioteca');
};
const activate=document.createElement('button');activate.className='secondary';
activate.textContent=enabled[id]?'DESACTIVAR':'ACTIVAR';
activate.disabled=!downloaded.includes(id);
activate.onclick=()=>{
enabled[id]=!enabled[id];write('craft_enabled',enabled);renderMarket();
toast(enabled[id]?'Complemento habilitado':'Complemento desactivado');
};
card.append(art,title,desc,status,download,activate);cards.append(card);
});
}
function openWorldAddonsForCurrent(){
if(!current){show('worldaddons');renderWorldAddonList();return}
selected=[...(current.addons||[])];
renderWorldAddonList();show('worldaddons');
}
function renderWorldAddonList(){
const list=document.getElementById('worldAddonList');list.replaceChildren();
Object.entries(definitions).forEach(([id,a])=>{
const row=document.createElement('label');row.className='check';
const check=document.createElement('input');check.type='checkbox';
check.checked=selected.includes(id);
check.disabled=!downloaded.includes(id)||!enabled[id];
check.onchange=()=>{
if(check.checked&&!selected.includes(id))selected.push(id);
else selected=selected.filter(x=>x!==id);
updateSummary();
};
const text=document.createElement('span');
text.textContent=a.name+(downloaded.includes(id)?'':' · Primero descarga')+(!enabled[id]?' · Actívalo en Marketplace':'');
row.append(check,text);list.append(row);
});
}
function updateSummary(){
document.getElementById('chosenSummary').textContent=
selected.length?'Seleccionados: '+selected.map(x=>definitions[x].name).join(', '):'Ningún complemento seleccionado.';
}
function saveWorldAddonChoice(){
if(current){
current.addons=[...selected];write('craft_worlds',worlds);
toast('Complementos del mundo guardados');
renderWorlds();show('worlds');
}else{
updateSummary();show('create');
}
}
function init3D(){
if(!window.THREE){toast('No se pudo cargar Three.js. Revisa tu conexión.');return false}
const canvas=document.getElementById('view');
renderer=new THREE.WebGLRenderer({canvas,antialias:true});
renderer.setPixelRatio(Math.min(devicePixelRatio,2));
renderer.setSize(innerWidth,innerHeight);
renderer.setClearColor(0x83c9ef);
scene=new THREE.Scene();scene.background=new THREE.Color(0x83c9ef);
scene.fog=new THREE.Fog(0x83c9ef,22,65);
camera=new THREE.PerspectiveCamera(75,innerWidth/innerHeight,.1,1000);
clock=new THREE.Clock();
scene.add(new THREE.HemisphereLight(0xffffff,0x65704c,2));
const sun=new THREE.DirectionalLight(0xffffff,2);sun.position.set(8,20,8);scene.add(sun);
blocks=[];const seed=12;
function cube(x,y,z,color,placed=false){
const material=new THREE.MeshLambertMaterial({color});
if(current&¤t.addons.includes('actions'))material.color.multiplyScalar(1.15);
const mesh=new THREE.Mesh(new THREE.BoxGeometry(1,1,1),material);
mesh.position.set(x,y,z);scene.add(mesh);blocks.push(mesh);return mesh;
}
for(let x=-14;x<=14;x++)for(let z=-14;z<=14;z++){
let h=Math.floor(0.7*Math.sin(x*.55)+0.8*Math.cos(z*.4));
for(let y=-2;y<=h;y++)cube(x,y,z,y===h?0x69b943:0x806044);
if((Math.abs(x*7+z*11)%47)===0&&Math.abs(x)+Math.abs(z)>4){
for(let y=1;y<=3;y++)cube(x,h+y,z,0x79502d);
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)+Math.abs(dy-3)<4)cube(x+dx,h+dy,z+dz,0x25843c);
}
}
// Agua decorativa
const water=new THREE.Mesh(new THREE.BoxGeometry(5,.12,4),new THREE.MeshLambertMaterial({color:0x349bd0,transparent:true,opacity:.8}));
water.position.set(7,-.05,-4);scene.add(water);
// Figura del jugador, cámara en primera persona
player=new THREE.Group();player.position.set(0,2,5);scene.add(player);
camera.position.set(0,0,0);player.add(camera);
// Verity: esfera amarilla
verityObject=null;huggyObject=null;
if(current&¤t.addons.includes('verity')){
verityObject=new THREE.Mesh(new THREE.SphereGeometry(.65,24,16),new THREE.MeshLambertMaterial({color:0xffd21c}));
verityObject.position.set(2,2,2);scene.add(verityObject);
const eyeMat=new THREE.MeshBasicMaterial({color:0x171717});
[-.2,.2].forEach(x=>{const eye=new THREE.Mesh(new THREE.SphereGeometry(.07,8,8),eyeMat);eye.position.set(x,.13,-.57);verityObject.add(eye)});
const mouth=new THREE.Mesh(new THREE.TorusGeometry(.18,.035,6,16,Math.PI),eyeMat);
mouth.position.set(0,-.12,-.58);mouth.rotation.z=Math.PI;verityObject.add(mouth);
}
// Huggy: modelo de prueba hecho con piezas 3D
if(current&¤t.addons.includes('huggy')){
huggyObject=new THREE.Group();const blue=new THREE.MeshLambertMaterial({color:0x168fe1});
function part(geo,x,y,z,mat=blue){const m=new THREE.Mesh(geo,mat);m.position.set(x,y,z);huggyObject.add(m)}
part(new THREE.BoxGeometry(.9,1.5,.5),0,1.2,0);
part(new THREE.SphereGeometry(.48,16,12),0,2.25,0);
part(new THREE.BoxGeometry(.25,1.1,.25),-.6,1.3,0);
part(new THREE.BoxGeometry(.25,1.1,.25),.6,1.3,0);
part(new THREE.BoxGeometry(.28,.9,.28),-.25,.25,0);
part(new THREE.BoxGeometry(.28,.9,.28),.25,.25,0);
const face=new THREE.MeshBasicMaterial({color:0xffffff});
part(new THREE.SphereGeometry(.07,8,8),-.16,2.3,-.42,face);
part(new THREE.SphereGeometry(.07,8,8),.16,2.3,-.42,face);
huggyObject.position.set(-3,0,-2);scene.add(huggyObject);
}
yaw=0;pitch=0;velocityY=0;
return true;
}
function startGame(w){
current=w;show('game');
document.getElementById('worldlabel').textContent=w.name+' · '+w.mode;
if(!init3D())return;
running=true;lastTime=0;requestAnimationFrame(loop);
}
function loop(time){
if(!running||!renderer)return;
requestAnimationFrame(loop);
const dt=Math.min(clock.getDelta(),.05);
const speed=Number(document.getElementById('speed')?.value||6)*dt;
let forward=(keys.KeyW||moveState.forward?1:0)-(keys.KeyS||moveState.back?1:0);
let side=(keys.KeyD||moveState.right?1:0)-(keys.KeyA||moveState.left?1:0);
player.position.x+=Math.sin(yaw)*forward*speed+Math.cos(yaw)*side*speed;
player.position.z-=Math.cos(yaw)*forward*speed-Math.sin(yaw)*side*speed;
player.position.x=Math.max(-13,Math.min(13,player.position.x));
player.position.z=Math.max(-13,Math.min(13,player.position.z));
velocityY-=18*dt;player.position.y+=velocityY*dt;
if(player.position.y<1.8){player.position.y=1.8;velocityY=0}
camera.rotation.order='YXZ';camera.rotation.y=yaw;camera.rotation.x=pitch;
if(verityObject){
verityObject.position.x=player.position.x+Math.sin(time*.001)*1.5+1.2;
verityObject.position.z=player.position.z+Math.cos(time*.001)*1.5+1.2;
verityObject.position.y=2.2+Math.sin(time*.003)*.2;
}
if(huggyObject){
huggyObject.position.x=-3+Math.sin(time*.0005)*.5;
huggyObject.position.y=Math.abs(Math.sin(time*.003))*.15;
}
renderer.render(scene,camera);
}
function jump(){if(player&&player.position.y<=1.81)velocityY=7}
function placeBlock(){
if(!scene||!player)return;
const dir=new THREE.Vector3(0,0,-1).applyEuler(camera.rotation);
const pos=player.position.clone().add(dir.multiplyScalar(3));
const geo=new THREE.BoxGeometry(1,1,1);
const color=current&¤t.addons.includes('actions')?0x93d56d:0x806044;
const mesh=new THREE.Mesh(geo,new THREE.MeshLambertMaterial({color}));
mesh.position.set(Math.round(pos.x),Math.max(0,Math.round(pos.y-1)),Math.round(pos.z));
scene.add(mesh);blocks.push(mesh);toast('Bloque colocado');
}
function breakBlock(){
if(!camera||!scene)return;
const ray=new THREE.Raycaster();ray.setFromCamera(new THREE.Vector2(0,0),camera);
const hit=ray.intersectObjects(blocks);
if(hit.length){scene.remove(hit[0].object);blocks=blocks.filter(b=>b!==hit[0].object);toast('Bloque roto')}
else toast('Apunta a un bloque cercano');
}
function pauseGame(){running=false;show('pause')}
function resumeGame(){show('game');running=true;clock.start();requestAnimationFrame(loop)}
function showInventory(){show('inventory')}
function exitWorld(){running=false;current=null;openWorlds()}
function openWorldAddonsForCurrent(){
if(!current){show('worldaddons');renderWorldAddonList();return}
selected=[...(current.addons||[])];renderWorldAddonList();show('worldaddons');
}
document.querySelectorAll('[data-move]').forEach(btn=>{
const k=btn.dataset.move;
const down=e=>{e.preventDefault();moveState[k]=true};
const up=e=>{e.preventDefault();moveState[k]=false};
btn.addEventListener('pointerdown',down);
btn.addEventListener('pointerup',up);
btn.addEventListener('pointerleave',up);
btn.addEventListener('pointercancel',up);
});
document.addEventListener('keydown',e=>{keys[e.code]=true;if(e.code==='Space')jump()});
document.addEventListener('keyup',e=>{keys[e.code]=false});
let dragging=false,px=0,py=0;
document.getElementById('view').addEventListener('pointerdown',e=>{
if(!running)return;dragging=true;px=e.clientX;py=e.clientY;
});
document.addEventListener('pointerup',()=>dragging=false);
document.addEventListener('pointermove',e=>{
if(!dragging||!running)return;
yaw-=(e.clientX-px)*.004;pitch-=(e.clientY-py)*.004;
pitch=Math.max(-1.4,Math.min(1.4,pitch));px=e.clientX;py=e.clientY;
});
addEventListener('resize',()=>{
if(renderer&&camera){renderer.setSize(innerWidth,innerHeight);camera.aspect=innerWidth/innerHeight;camera.updateProjectionMatrix()}
});
renderMarket();
</script>
</body>
</html>
Game Source: Craft 3D
Creator: MagicKnight13
Libraries: three
Complexity: complex (480 lines, 21.4 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: craft-3d-magicknight13" to link back to the original. Then publish at arcadelab.ai/publish.