Craft
by MagicKnight13607 lines27.8 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</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&¤t.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&¤t.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.