🎮ArcadeLab

Craft

by MagicKnight13
607 lines27.8 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, user-scalable=no">
<title>Craft</title>
<style>
*{box-sizing:border-box}
body{margin:0;background:#171717;color:white;font-family:Arial,sans-serif}
button,input,select{font:inherit}
button{border:2px solid #333;background:#777;color:white;padding:12px 15px;font-weight:bold;cursor:pointer;box-shadow:0 3px #222}
button:active{transform:translateY(2px);box-shadow:none}
button:disabled{opacity:.4}
.primary{background:#4d8c36}
.secondary{background:#555}
.screen{display:none;min-height:100vh;padding:20px;background:linear-gradient(#76b9e7,#a7d5ef 45%,#405d2c)}
.screen.active{display:flex;align-items:center;justify-content:center}
.panel{width:min(650px,100%);padding:20px;background:#292929ed;border:3px solid #777;box-shadow:0 6px #151515}
h1{text-align:center;font-size:clamp(36px,8vw,60px);margin:8px;text-shadow:4px 4px #222}
h2{margin-top:0}
.menu-buttons{display:grid;gap:10px;margin-top:20px}
.menu-buttons button{width:100%;min-height:48px}
.small{font-size:13px;color:#ddd;line-height:1.5}
.card{padding:13px;margin:10px 0;background:#414141;border:2px solid #777}
.card h3{margin:0 0 8px}
.row{display:flex;gap:8px;align-items:center;flex-wrap:wrap}
.row button{flex:1}
#game{display:none;position:fixed;inset:0;background:#87ceeb;overflow:hidden}
#view{width:100%;height:100%;display:block;touch-action:none}
#hud{position:absolute;inset:0;pointer-events:none}
#topbar{position:absolute;top:10px;left:10px;right:10px;display:flex;justify-content:space-between;gap:8px}
#topbar button,#controls button,#crosshair,#hand,#hotbar,#toast{pointer-events:auto}
#crosshair{position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);font-size:24px;text-shadow:0 1px 3px black}
#hand{position:absolute;bottom:13%;right:8%;width:95px;height:125px;transform-origin:bottom right;pointer-events:none}
#hand:before{content:"";position:absolute;right:8px;bottom:0;width:54px;height:95px;background:#e1b18a;clip-path:polygon(15% 100%,0 35%,25% 0,43% 30%,58% 0,73% 35%,100% 25%,90% 100%);border:3px solid #68472e}
#hotbar{position:absolute;bottom:12px;left:50%;transform:translateX(-50%);display:flex;gap:3px;max-width:95%;overflow:auto}
.slot{width:43px;height:43px;flex-shrink:0;background:#292929c9;border:2px solid #aaa;text-align:center;padding:4px;font-size:10px}
.slot.sel{border:3px solid white}
#controls{position:absolute;left:12px;bottom:80px;display:grid;grid-template-columns:repeat(3,48px);gap:5px;pointer-events:auto}
#controls button{padding:10px 0;background:#333b}
#actions{position:absolute;right:12px;bottom:80px;display:grid;gap:6px;pointer-events:auto}
#actions button{padding:10px;background:#333b}
#toast{position:absolute;top:65px;left:50%;transform:translateX(-50%);background:#222;padding:10px;display:none;max-width:90%;text-align:center}
#inventory{display:none;position:absolute;inset:5%;overflow:auto;background:#222f;border:3px solid #aaa;padding:15px;pointer-events:auto}
#catalog{display:grid;grid-template-columns:repeat(3,1fr);gap:7px}
#catalog button{font-size:12px;padding:10px 3px}
#inventory h2{margin:0 0 10px}
@media(max-width:480px){.screen{padding:10px}.panel{padding:14px}#hand{width:70px;height:90px}#hand:before{transform:scale(.75);transform-origin:bottom right}}
</style>
</head>
<body>

<section id="main" class="screen active">
<div class="panel">
<h1>CRAFT</h1>
<p style="text-align:center">Construye tu propio mundo</p>
<div class="menu-buttons">
<button class="primary" onclick="show('worlds')">JUGAR</button>
<button onclick="show('multiplayer')">JUGAR EN MULTIJUGADOR</button>
<button onclick="show('market')">MARKETPLACE</button>
<button onclick="show('skin')">VESTIDOR</button>
<button onclick="show('settings')">AJUSTES</button>
</div>
<p class="small">Prototipo 3D para navegador. No es Minecraft oficial.</p>
</div>
</section>

<section id="worlds" class="screen">
<div class="panel">
<h2>Seleccionar mundo</h2>
<div class="row">
<button class="primary" onclick="quick('Creativo')">CREATIVO</button>
<button class="primary" onclick="quick('Supervivencia')">SUPERVIVENCIA</button>
</div>
<div class="card">
<h3>Crear mundo personalizado</h3>
<input id="worldName" placeholder="Nombre del mundo" value="Mi mundo" style="width:100%;padding:10px">
<p><select id="worldMode" style="width:100%;padding:10px">
<option>Creativo</option><option>Supervivencia</option>
</select></p>
<button onclick="createCustom()">CREAR MUNDO</button>
</div>
<div id="worldList"></div>
<button onclick="show('main')">VOLVER</button>
</div>
</section>

<section id="create" class="screen">
<div class="panel">
<h2>Crear mundo</h2>
<label>Nombre</label>
<input id="draftName" value="Mi mundo" style="width:100%;padding:10px">
<p>Modo</p>
<select id="draftMode" style="width:100%;padding:10px"><option>Creativo</option><option>Supervivencia</option></select>
<button onclick="openDraftAddons()">COMPLEMENTOS DEL MUNDO</button>
<div id="draftSummary" class="small"></div>
<button class="primary" onclick="createWorld()">CREAR</button>
<button onclick="show('worlds')">CANCELAR</button>
</div>
</section>

<section id="worldaddons" class="screen">
<div class="panel">
<h2>Complementos del mundo</h2>
<p class="small">Descarga y activa complementos en Marketplace primero. Luego selecciona cuáles usar en este mundo.</p>
<div id="addonChoices"></div>
<button class="primary" onclick="saveWorldAddonChoice()">GUARDAR SELECCIÓN</button>
<button onclick="show('market')">IR AL MARKETPLACE</button>
</div>
</section>

<section id="market" class="screen">
<div class="panel">
<h2>Marketplace</h2>
<div id="marketCards"></div>
<button onclick="show('main')">VOLVER</button>
</div>
</section>

<section id="multiplayer" class="screen">
<div class="panel">
<h2>Multijugador</h2>
<p>El multijugador en línea todavía no está implementado.</p>
<button onclick="show('main')">VOLVER</button>
</div>
</section>

<section id="settings" class="screen">
<div class="panel">
<h2>Ajustes</h2>
<p>Controles: WASD o flechas para moverte, espacio para saltar. En móvil usa los botones.</p>
<p>Arrastra la pantalla para mirar alrededor. Rompe y coloca bloques desde los botones de juego.</p>
<button onclick="show('main')">VOLVER</button>
</div>
</section>

<section id="skin" class="screen">
<div class="panel">
<h2>Vestidor</h2>
<p>La personalización de skins todavía no está implementada.</p>
<button onclick="show('main')">VOLVER</button>
</div>
</section>

<div id="game">
<canvas id="view"></canvas>
<div id="hud">
<div id="topbar">
<button onclick="pauseGame()">Ⅱ PAUSA</button>
<div id="worldLabel" style="background:#222a;padding:10px">Craft</div>
<button onclick="openInventory()">INVENTARIO</button>
</div>
<div id="crosshair">+</div>
<div id="hand"></div>
<div id="hotbar"></div>
<div id="controls">
<div></div><button data-move="forward">▲</button><div></div>
<button data-move="left">◀</button><button data-move="back">▼</button><button data-move="right">▶</button>
</div>
<div id="actions">
<button onclick="breakBlock()">ROMPER</button>
<button onclick="placeBlock()">COLOCAR</button>
<button onclick="jump()">SALTAR</button>
</div>
<div id="toast"></div>
<div id="inventory">
<div class="row"><h2 style="flex:1">Inventario</h2><button onclick="closeInventory()">CERRAR</button></div>
<p id="inventoryStatus" class="small"></p>
<div id="playerItems" class="card"></div>
<h3>Bloques disponibles</h3>
<div id="catalog"></div>
</div>
</div>
</div>

<div id="pauseScreen" class="screen" style="position:fixed;inset:0;z-index:10">
<div class="panel">
<h2>Juego en pausa</h2>
<button class="primary" onclick="resumeGame()">CONTINUAR</button>
<button onclick="exitWorld()">GUARDAR Y SALIR</button>
</div>
</div>

<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
<script>
const addonInfo = {
  verity: {name:"Verity",desc:"La caja misteriosa, Verity y el evento del monstruo."},
  huggy: {name:"Huggy & sus amigos",desc:"La fábrica, Huggy y las manos del GrabPack."},
  actions: {name:"Actions & Stuff",desc:"Cambios visuales y animaciones del mundo."}
};

let downloaded = loadJSON("craft_downloaded", []);
let enabled = loadJSON("craft_enabled", []);
let worlds = loadJSON("craft_worlds", []);
let current = null;
let draft = {name:"Mi mundo",mode:"Creativo",addons:[]};
let selectedAddons = [];
let draftMode = false;
let renderer, scene, camera, clock, player, velocity;
let blocks = new Map();
let addonObjects = [];
let running = false;
let lastTime = 0;
let daySeconds = 0;
let verityStage = 0;
let verityAge = 0;
let huggyStage = 0;
let handSwing = 0;
let isMoving = false;
let inventory = [];
let chosenBlock = "Tierra";
let yaw = 0, pitch = 0;
let keys = {};
let mobile = {};
let lastLookX = null, lastLookY = null;
const BLOCKS = [
 ["Tierra",0x795034],["Césped",0x559b39],["Piedra",0x858585],
 ["Madera",0x8b5a2b],["Hojas",0x347d35],["Arena",0xd8c27b],
 ["Ladrillos",0x98594b],["Vidrio",0x9bdce8],["Agua",0x357ad3],
 ["Lana",0xe6e6e6],["Oro",0xe8bd35],["Hierro",0xc3c8ca]
];
const $ = id => document.getElementById(id);
function loadJSON(k,f){try{return JSON.parse(localStorage.getItem(k))||f}catch(e){return f}}
function persist(){try{
 localStorage.setItem("craft_downloaded",JSON.stringify(downloaded));
 localStorage.setItem("craft_enabled",JSON.stringify(enabled));
 localStorage.setItem("craft_worlds",JSON.stringify(worlds));
}catch(e){}}
function show(id){
 document.querySelectorAll(".screen").forEach(e=>e.classList.remove("active"));
 $("game").style.display="none";
 if($(id))$(id).classList.add("active");
 if(id==="worlds")renderWorlds();
 if(id==="market")renderMarket();
}
function toast(msg){
 const el=$("toast");el.textContent=msg;el.style.display="block";
 clearTimeout(toast.timer);toast.timer=setTimeout(()=>el.style.display="none",3000);
}
function renderMarket(){
 $("marketCards").innerHTML="";
 Object.keys(addonInfo).forEach(id=>{
  const a=addonInfo[id],dl=downloaded.includes(id),on=enabled.includes(id);
  const div=document.createElement("div");div.className="card";
  const title=document.createElement("h3");title.textContent=a.name;div.appendChild(title);
  const p=document.createElement("p");p.textContent=a.desc;div.appendChild(p);
  const row=document.createElement("div");row.className="row";
  const b=document.createElement("button");b.textContent=dl?"DESCARGADO":"DESCARGAR";
  b.disabled=dl;b.onclick=()=>{downloaded.push(id);persist();renderMarket();toast(a.name+" descargado");};
  row.appendChild(b);
  const t=document.createElement("button");t.textContent=on?"DESACTIVAR":"ACTIVAR";
  t.disabled=!dl;t.onclick=()=>{
   enabled=on?enabled.filter(x=>x!==id):[...enabled,id];
   persist();renderMarket();renderWorlds();
   toast(a.name+(on?" desactivado":" activado"));
  };
  row.appendChild(t);div.appendChild(row);
  const status=document.createElement("p");status.className="small";
  status.textContent=!dl?"Primero descarga este complemento.":on?"Activado globalmente; selecciónalo en tu mundo.":"Descargado; falta activarlo.";
  div.appendChild(status);$("marketCards").appendChild(div);
 });
}
function renderWorlds(){
 $("worldList").innerHTML="";
 if(!worlds.length){$("worldList").innerHTML='<p class="small">Todavía no tienes mundos guardados.</p>';return;}
 worlds.forEach(w=>{
  const div=document.createElement("div");div.className="card";
  const h=document.createElement("h3");h.textContent=w.name;div.appendChild(h);
  const p=document.createElement("p");p.textContent=w.mode+" · Complementos: "+(w.addons.length?w.addons.map(x=>addonInfo[x].name).join(", "):"ninguno");div.appendChild(p);
  const row=document.createElement("div");row.className="row";
  const play=document.createElement("button");play.className="primary";play.textContent="JUGAR";
  play.onclick=()=>loadWorld(w.id);row.appendChild(play);
  const mods=document.createElement("button");mods.textContent="COMPLEMENTOS";
  mods.onclick=()=>openExistingAddons(w.id);row.appendChild(mods);
  const del=document.createElement("button");del.textContent="BORRAR";
  del.onclick=()=>{worlds=worlds.filter(x=>x.id!==w.id);persist();renderWorlds();};row.appendChild(del);
  div.appendChild(row);$("worldList").appendChild(div);
 });
}
function quick(mode){draft={name:mode==="Creativo"?"Mundo creativo":"Mundo supervivencia",mode,addons:[]};createWorld()}
function createCustom(){
 draft={name:$("worldName").value.trim()||"Mi mundo",mode:$("worldMode").value,addons:[]};
 $("draftName").value=draft.name;$("draftMode").value=draft.mode;
 show("create");updateDraftSummary();
}
function updateDraftSummary(){
 $("draftSummary").textContent="Complementos seleccionados: "+(draft.addons.length?draft.addons.map(x=>addonInfo[x].name).join(", "):"ninguno");
}
function openDraftAddons(){
 draftMode=true;selectedAddons=[...draft.addons];renderAddonChoices();show("worldaddons");
}
function openExistingAddons(id){
 current=worlds.find(w=>w.id===id);draftMode=false;
 selectedAddons=current?[...current.addons]:[];renderAddonChoices();show("worldaddons");
}
function renderAddonChoices(){
 $("addonChoices").innerHTML="";
 Object.keys(addonInfo).forEach(id=>{
  const usable=downloaded.includes(id)&&enabled.includes(id);
  const div=document.createElement("div");div.className="card";
  const label=document.createElement("label");
  const cb=document.createElement("input");cb.type="checkbox";cb.checked=selectedAddons.includes(id);
  cb.disabled=!usable;cb.onchange=()=>{selectedAddons=cb.checked?[...selectedAddons,id]:selectedAddons.filter(x=>x!==id);};
  label.appendChild(cb);label.append(" "+addonInfo[id].name);
  div.appendChild(label);
  const p=document.createElement("p");p.className="small";
  p.textContent=usable?addonInfo[id].desc:"Debes descargar y activar este complemento en Marketplace.";
  div.appendChild(p);$("addonChoices").appendChild(div);
 });
}
function saveWorldAddonChoice(){
 selectedAddons=selectedAddons.filter(id=>downloaded.includes(id)&&enabled.includes(id));
 if(draftMode){
  draft.addons=[...selectedAddons];
  draft.name=$("draftName").value.trim()||draft.name;
  draft.mode=$("draftMode").value;
  updateDraftSummary();show("create");
 }else if(current){
  current.addons=[...selectedAddons];persist();renderWorlds();show("worlds");
 }else show("worlds");
}
function createWorld(){
 if($("draftName").value.trim())draft.name=$("draftName").value.trim();
 if($("draftMode").value)draft.mode=$("draftMode").value;
 const w={id:Date.now()+Math.random(),name:draft.name||"Mi mundo",mode:draft.mode||"Creativo",addons:[...draft.addons]};
 worlds.push(w);persist();current=w;draftMode=false;startGame(w);
}
function loadWorld(id){const w=worlds.find(x=>x.id===id);if(w){current=w;startGame(w)}}
function startGame(w){
 if(typeof THREE==="undefined"){alert("No se pudo cargar Three.js. Revisa tu conexión a Internet.");return;}
 $("pauseScreen").classList.remove("active");$("game").style.display="block";
 $("worldLabel").textContent=w.name+" · "+w.mode;
 inventory=[];chosenBlock="Tierra";daySeconds=0;verityAge=0;verityStage=0;huggyStage=0;
 init3D();running=true;clock=new THREE.Clock();lastTime=performance.now();
 renderHotbar();applyAddons(w);requestAnimationFrame(loop);
}
function init3D(){
 try{
  if(renderer){renderer.dispose();renderer=null;}
  scene=new THREE.Scene();scene.background=new THREE.Color(0x87ceeb);
  scene.fog=new THREE.Fog(0x87ceeb,20,75);
  camera=new THREE.PerspectiveCamera(75,innerWidth/innerHeight,.1,150);
  renderer=new THREE.WebGLRenderer({canvas:$("view"),antialias:true});
  renderer.setPixelRatio(Math.min(devicePixelRatio,1.6));renderer.setSize(innerWidth,innerHeight);
  scene.add(new THREE.HemisphereLight(0xffffff,0x6d8050,1.5));
  const sun=new THREE.DirectionalLight(0xffffff,1.1);sun.position.set(-5,12,7);scene.add(sun);
  blocks.clear();addonObjects=[];
  const geo=new THREE.BoxGeometry(1,1,1);
  function addBlock(x,y,z,name){
   const data=BLOCKS.find(b=>b[0]===name)||BLOCKS[0];
   const mat=new THREE.MeshLambertMaterial({color:data[1],transparent:name==="Vidrio"||name==="Agua",opacity:name==="Vidrio"?.32:name==="Agua"?.65:1});
   const m=new THREE.Mesh(geo,mat);m.position.set(x,y,z);m.userData={block:true,name,x,y,z};scene.add(m);blocks.set(`${x},${y},${z}`,m);return m;
  }
  for(let x=-14;x<=14;x++)for(let z=-14;z<=14;z++){
   addBlock(x,-1,z,"Tierra");
   addBlock(x,0,z,Math.random()<.12?"Tierra":"Césped");
  }
  for(let i=0;i<12;i++){
   const x=Math.floor(Math.random()*23)-11,z=Math.floor(Math.random()*23)-11;
   for(let y=1;y<=3;y++)addBlock(x,y,z,"Madera");
   for(let dx=-1;dx<=1;dx++)for(let dz=-1;dz<=1;dz++)for(let y=3;y<=4;y++){
    if(Math.abs(dx)+Math.abs(dz)<3)addBlock(x+dx,y,z+dz,"Hojas");
   }
  }
  player={position:new THREE.Vector3(0,1,5),onGround:false};
  velocity=new THREE.Vector3();
  yaw=0;pitch=0;
  camera.position.set(0,2.62,5);camera.rotation.order="YXZ";
  createHand();
 }catch(e){toast("Error al crear el mundo 3D: "+e.message)}
}
function createHand(){
 const el=$("hand");el.style.display="block";
}
function applyAddons(w){
 const addons=w.addons.filter(id=>downloaded.includes(id)&&enabled.includes(id));
 $("hand").style.filter=addons.includes("huggy")?"hue-rotate(155deg)":"none";
 if(addons.includes("verity"))spawnBox();
 if(addons.includes("huggy"))spawnHuggy();
 if(addons.includes("actions"))toast("Actions & Stuff activado: texturas y apariencia modificadas.");
}
let verityBox=null,verity=null,huggy=null;
function clearOldAddonObjects(){
 [verityBox,verity,huggy].forEach(o=>{if(o&&scene)scene.remove(o)});
 verityBox=verity=huggy=null;
}
function spawnBox(){
 clearOldAddonObjects();
 verityStage=0;verityAge=0;
 verityBox=new THREE.Mesh(new THREE.BoxGeometry(.8,.8,.8),new THREE.MeshLambertMaterial({color:0x9d6c3c}));
 verityBox.position.set(.9,.4,4.3);scene.add(verityBox);
 toast("Ha aparecido una caja junto a ti. Acércate y pulsa ROMPER.");
}
function spawnVerity(){
 if(verityBox){scene.remove(verityBox);verityBox=null}
 const group=new THREE.Group();
 const ball=new THREE.Mesh(new THREE.SphereGeometry(.48,20,16),new THREE.MeshLambertMaterial({color:0xffdd16}));
 ball.position.y=.55;group.add(ball);
 const eyeMat=new THREE.MeshBasicMaterial({color:0x111111});
 for(const x of [-.16,.16]){
  const eye=new THREE.Mesh(new THREE.SphereGeometry(.055,8,8),eyeMat);eye.position.set(x,.65,.4);group.add(eye);
 }
 const mouth=new THREE.Mesh(new THREE.TorusGeometry(.14,.025,6,16,Math.PI),eyeMat);
 mouth.position.set(0,.46,.45);mouth.rotation.z=Math.PI;group.add(mouth);
 group.position.set(.9,0,3.6);scene.add(group);verity=group;
 verityStage=1;verityAge=0;
 toast("Hello, soy Verity. Hola, soy Verity, tu amigo ayudante personal. Pregúntame lo que quieras. Yo lo sé todo.");
}
function spawnHuggy(){
 if(huggy)scene.remove(huggy);
 const g=new THREE.Group();
 const blue=new THREE.MeshLambertMaterial({color:0x1764d2});
 const body=new THREE.Mesh(new THREE.BoxGeometry(.65,1.15,.42),blue);body.position.y=.85;g.add(body);
 const head=new THREE.Mesh(new THREE.BoxGeometry(.65,.65,.55),blue);head.position.y=1.65;g.add(head);
 const face=new THREE.Mesh(new THREE.BoxGeometry(.34,.16,.03),new THREE.MeshBasicMaterial({color:0x171717}));face.position.set(0,1.58,.29);g.add(face);
 for(const x of [-.5,.5]){
  const arm=new THREE.Mesh(new THREE.BoxGeometry(.18,.9,.18),blue);arm.position.set(x,.9,0);g.add(arm);
  const leg=new THREE.Mesh(new THREE.BoxGeometry(.2,.65,.22),blue);leg.position.set(x*.55,.32,0);g.add(leg);
 }
 g.position.set(2,0,-2);scene.add(g);huggy=g;
 toast("Huggy te espera en la fábrica. Pulsa ROMPER cuando estés cerca para superar el encuentro.");
}
function solid(x,y,z){return blocks.has(`${x},${y},${z}`)}
const R=.29,H=1.75;
function canMove(x,y,z){
 for(let bx=Math.floor(x-R);bx<=Math.floor(x+R);bx++)
 for(let by=Math.floor(y);by<=Math.floor(y+H);by++)
 for(let bz=Math.floor(z-R);bz<=Math.floor(z+R);bz++){
  if(solid(bx,by,bz)&&x+R>bx&&x-R<bx+1&&y+H>by&&y<by+1&&z+R>bz&&z-R<bz+1)return false;
 }
 return true;
}
function tryMove(dx,dy,dz){
 let p=camera.position;
 if(canMove(p.x+dx,p.y-1.62,p.z)){p.x+=dx}
 if(canMove(p.x,p.y-1.62+dy,p.z)){p.y+=dy}
 if(canMove(p.x,p.y-1.62,p.z+dz)){p.z+=dz}
}
function loop(now){
 if(!running||!renderer)return;
 requestAnimationFrame(loop);
 const dt=Math.min((now-lastTime)/1000,.05);lastTime=now;
 if(!clock)clock=new THREE.Clock();
 if(! $("pauseScreen").classList.contains("active") && $("inventory").style.display!=="block"){
  daySeconds+=dt;
  updatePlayer(dt);
  updateEvents(dt);
 }
 renderer.render(scene,camera);
}
function updatePlayer(dt){
 const forward=(keys.KeyW||keys.ArrowUp||mobile.forward?1:0)-(keys.KeyS||keys.ArrowDown||mobile.back?1:0);
 const side=(keys.KeyD||keys.ArrowRight||mobile.right?1:0)-(keys.KeyA||keys.ArrowLeft||mobile.left?1:0);
 isMoving=!!(forward||side);
 const speed=current&&current.mode==="Creativo"?5.2:4.1;
 const fx=-Math.sin(yaw),fz=-Math.cos(yaw),rx=Math.cos(yaw),rz=-Math.sin(yaw);
 let dx=(fx*forward+rx*side)*speed*dt;
 let dz=(fz*forward+rz*side)*speed*dt;
 const p=camera.position;
 if(isMoving){tryMove(dx,0,dz);handSwing+=dt*10;$("hand").style.transform=`rotate(${Math.sin(handSwing)*5}deg) translateY(${Math.abs(Math.sin(handSwing))*5}px)`;}
 else $("hand").style.transform="none";
 velocity.y-=19*dt;
 if(canMove(p.x,p.y-1.62+velocity.y*dt,p.z)){p.y+=velocity.y*dt;player.onGround=false}
 else{if(velocity.y<0)player.onGround=true;velocity.y=0}
 if(p.y<1.65){p.y=1.65;velocity.y=0;player.onGround=true}
 camera.rotation.set(pitch,yaw,0,"YXZ");
 if(verity)verity.rotation.y+=dt*.4;
 if(huggy)huggy.rotation.y=Math.sin(now/800)*.15;
}
function updateEvents(dt){
 if(!current)return;
 const addons=current.addons.filter(id=>downloaded.includes(id)&&enabled.includes(id));
 if(addons.includes("verity")){
  verityAge+=dt;
  // Un día de juego dura 20 minutos reales.
  const days=verityAge/1200;
  if(verityStage===1&&days>=5){
   verityStage=2;toast("Verity: Algo llegará dentro de tres días...");
  }
  if(verityStage===2&&days>=8){
   verityStage=3;
   const monster=new THREE.Mesh(new THREE.BoxGeometry(1,2.3,1),new THREE.MeshLambertMaterial({color:0x29152e}));
   monster.position.set(camera.position.x+2,1,camera.position.z-5);scene.add(monster);verity=monster;
   toast("¡El monstruo de Verity ha llegado!");
  }
  if(verityStage===3&&verity){
   const target=new THREE.Vector3(camera.position.x,1,camera.position.z);
   const dir=target.clone().sub(verity.position);dir.y=0;
   if(dir.length()>0.1){dir.normalize();verity.position.addScaledVector(dir,dt*1.5)}
   if(verity.position.distanceTo(camera.position)<1.1){running=false;toast("El monstruo te alcanzó. Reinicia el mundo para volver a jugar.");}
  }
 }
 if(addons.includes("huggy")&&huggy){
  const d=huggy.position.distanceTo(new THREE.Vector3(camera.position.x,0,camera.position.z));
  if(d<1.5&&huggyStage===0)toast("Huggy está cerca. Pulsa ROMPER para superar el encuentro.");
 }
}
function jump(){if(player&&player.onGround){velocity.y=7.3;player.onGround=false}}
function getTargetBlock(){
 const ray=new THREE.Raycaster();ray.setFromCamera(new THREE.Vector2(0,0),camera);
 const hits=ray.intersectObjects([...blocks.values()]);
 return hits.find(h=>h.distance<5);
}
function breakBlock(){
 if(verityBox){
  const d=verityBox.position.distanceTo(camera.position);
  if(d<3){spawnVerity();return}
 }
 if(huggy&&huggyStage===0){
  const d=huggy.position.distanceTo(new THREE.Vector3(camera.position.x,0,camera.position.z));
  if(d<3){
   scene.remove(huggy);huggy=null;huggyStage=1;
   inventory.push("Mano roja");renderHotbar();toast("¡Huggy superado! Has desbloqueado la mano roja.");return
  }
 }
 const hit=getTargetBlock();if(!hit){toast("Mira hacia un bloque cercano.");return}
 const o=hit.object;scene.remove(o);blocks.delete(`${o.userData.x},${o.userData.y},${o.userData.z}`);
 toast("Bloque roto");
}
function placeBlock(){
 if(!inventory.length&&current.mode==="Creativo"){toast("Tu inventario está vacío. Abre INVENTARIO para elegir bloques.");return}
 const ray=new THREE.Raycaster();ray.setFromCamera(new THREE.Vector2(0,0),camera);
 const hits=ray.intersectObjects([...blocks.values()]);
 const hit=hits.find(h=>h.distance<5);if(!hit){toast("Mira hacia una superficie.");return}
 const n=hit.face.normal.clone().transformDirection(hit.object.matrixWorld);
 const pos=hit.object.position.clone().add(n);
 const x=Math.round(pos.x),y=Math.round(pos.y),z=Math.round(pos.z);
 if(solid(x,y,z))return;
 const feetY=camera.position.y-1.62;
 if(Math.abs(x-camera.position.x)<.8&&Math.abs(z-camera.position.z)<.8&&y>=Math.floor(feetY)&&y<=Math.floor(feetY+H)){toast("No puedes colocar un bloque dentro de ti.");return}
 const data=BLOCKS.find(b=>b[0]===chosenBlock)||BLOCKS[0];
 const m=new THREE.Mesh(new THREE.BoxGeometry(1,1,1),new THREE.MeshLambertMaterial({color:data[1]}));
 m.position.set(x,y,z);m.userData={block:true,name:chosenBlock,x,y,z};scene.add(m);blocks.set(`${x},${y},${z}`,m);
 if(current.mode!=="Creativo"){const i=inventory.indexOf(chosenBlock);if(i>=0)inventory.splice(i,1);renderHotbar()}
}
function openInventory(){
 $("inventory").style.display="block";renderInventory();
}
function closeInventory(){$("inventory").style.display="none"}
function renderInventory(){
 $("inventoryStatus").textContent=current.mode==="Creativo"?"Creativo: empiezas con cero objetos. Selecciona bloques del catálogo.":"Supervivencia: usa los objetos que hayas conseguido.";
 $("playerItems").textContent="Inventario: "+(inventory.length?inventory.join(", "):"vacío");
 $("catalog").innerHTML="";
 BLOCKS.forEach(([name,color])=>{
  const b=document.createElement("button");b.textContent=name;b.style.borderColor="#"+color.toString(16).padStart(6,"0");
  b.onclick=()=>{
   if(current.mode==="Creativo"){inventory.push(name);chosenBlock=name;toast(name+" añadido al inventario");}
   else if(inventory.includes(name)){chosenBlock=name;toast("Seleccionado: "+name)}
   else toast("No tienes ese bloque en Supervivencia.");
   renderInventory();renderHotbar();
  };$("catalog").appendChild(b);
 });
}
function renderHotbar(){
 $("hotbar").innerHTML="";
 for(let i=0;i<5;i++){
  const name=inventory[i]||"";
  const b=document.createElement("button");b.className="slot"+(name===chosenBlock?" sel":"");
  b.textContent=name||"—";b.onclick=()=>{if(name){chosenBlock=name;renderHotbar()}};
  $("hotbar").appendChild(b);
 }
}
function pauseGame(){running=false;$("pauseScreen").classList.add("active")}
function resumeGame(){$("pauseScreen").classList.remove("active");running=true;lastTime=performance.now();requestAnimationFrame(loop)}
function exitWorld(){
 running=false;$("game").style.display="none";$("pauseScreen").classList.remove("active");
 current=null;show("worlds");
}
window.addEventListener("keydown",e=>{
 keys[e.code]=true;
 if(["Space","ArrowUp","ArrowDown","ArrowLeft","ArrowRight"].includes(e.code))e.preventDefault();
 if(e.code==="Space")jump();
 if(e.code==="KeyE")openInventory();
 if(e.code==="Escape"){if($("inventory").style.display==="block")closeInventory();else pauseGame()}
});
window.addEventListener("keyup",e=>keys[e.code]=false);
document.querySelectorAll("[data-move]").forEach(b=>{
 const key=b.dataset.move;
 const start=e=>{e.preventDefault();mobile[key]=true};
 const stop=e=>{e.preventDefault();mobile[key]=false};
 b.addEventListener("pointerdown",start);b.addEventListener("pointerup",stop);
 b.addEventListener("pointerleave",stop);b.addEventListener("pointercancel",stop);
});
$("view").addEventListener("pointerdown",e=>{
 if(e.pointerType==="mouse"&&e.button!==0)return;
 lastLookX=e.clientX;lastLookY=e.clientY;
 if($("game").style.display==="block")$("view").setPointerCapture?.(e.pointerId);
});
$("view").addEventListener("pointermove",e=>{
 if(lastLookX===null||$("inventory").style.display==="block")return;
 const dx=e.clientX-lastLookX,dy=e.clientY-lastLookY;
 yaw-=dx*.004;pitch-=dy*.004;
 pitch=Math.max(-1.45,Math.min(1.45,pitch));
 lastLookX=e.clientX;lastLookY=e.clientY;
});
function stopLook(){lastLookX=lastLookY=null}
$("view").addEventListener("pointerup",stopLook);
$("view").addEventListener("pointercancel",stopLook);
window.addEventListener("resize",()=>{
 if(renderer&&camera){camera.aspect=innerWidth/innerHeight;camera.updateProjectionMatrix();renderer.setSize(innerWidth,innerHeight)}
});
renderMarket();renderWorlds();
</script>
</body>
</html>

Game Source: Craft

Creator: MagicKnight13

Libraries: three

Complexity: complex (607 lines, 27.8 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-magicknight13" to link back to the original. Then publish at arcadelab.ai/publish.