🎮ArcadeLab

Slendytubbies 1 - Juego 3D

by MagicKnight13
457 lines14.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,user-scalable=no">
<title>Slendytubbies 1 - Juego 3D</title>
<style>
*{box-sizing:border-box}
body{margin:0;overflow:hidden;background:#101510;color:white;font-family:Arial}
canvas{display:block}
#hud{position:fixed;top:8px;left:8px;z-index:5;padding:10px;background:#000b;border-radius:8px;font-size:13px;line-height:1.7;pointer-events:none}
#message{position:fixed;top:42%;left:5%;width:90%;text-align:center;font-size:22px;font-weight:bold;text-shadow:2px 2px 5px black;pointer-events:none;z-index:4}
#mobile{position:fixed;bottom:12px;left:10px;z-index:8;display:grid;grid-template-columns:55px 55px 55px;gap:4px}
#mobile button{margin:0;padding:12px 0;background:#171b18dd;color:white;border:0;border-radius:8px;font-size:21px;touch-action:none}
#actions{position:fixed;right:10px;bottom:20px;z-index:8;display:flex;flex-direction:column;gap:7px}
#actions button{background:#382449e8;color:white;border:0;border-radius:8px;padding:12px;font-size:13px;touch-action:manipulation}
#crosshair{position:fixed;left:50%;top:50%;transform:translate(-50%,-50%);pointer-events:none;z-index:3}
</style>
</head>
<body>

<div id="hud">
<b>SLENDYTUBBIES 1</b><br>
Ambiente: <span id="time">Día</span><br>
Papillas: <span id="count">0</span>/10<br>
Linterna: <span id="battery">100</span>%<br>
Criatura: <span id="danger">Lejos</span><br>
Lugar: <span id="place">Bosque</span>
</div>

<div id="message">Encuentra las 10 papillas moradas.</div>
<div id="crosshair">+</div>

<div id="mobile">
<span></span><button data-key="KeyW">▲</button><span></span>
<button data-key="KeyA">◀</button><button data-key="KeyS">▼</button><button data-key="KeyD">▶</button>
</div>

<div id="actions">
<button id="flash">🔦 Linterna</button>
<button id="sprint">🏃 Correr</button>
<button id="pause">Ⅱ Pausa</button>
</div>

<script src="https://cdn.jsdelivr.net/npm/three@0.160.0/build/three.min.js"></script>
<script>
(function(){
"use strict";

if(!window.THREE){
 document.getElementById("message").textContent=
 "No se pudo cargar el motor 3D. Comprueba tu conexión a Internet.";
 return;
}

const scene=new THREE.Scene();
scene.background=new THREE.Color(0x91b4c9);
scene.fog=new THREE.Fog(0x91b4c9,35,150);

const camera=new THREE.PerspectiveCamera(75,innerWidth/innerHeight,.1,400);
camera.position.set(0,1.7,8);

const renderer=new THREE.WebGLRenderer({antialias:true});
renderer.setSize(innerWidth,innerHeight);
renderer.setPixelRatio(Math.min(devicePixelRatio,1.5));
document.body.appendChild(renderer.domElement);

const hemi=new THREE.HemisphereLight(0xb8d9ed,0x303a22,1.4);
scene.add(hemi);

const sun=new THREE.DirectionalLight(0xffe3b2,1.8);
sun.position.set(30,55,15);
sun.castShadow=true;
scene.add(sun);

const flashlight=new THREE.SpotLight(0xffffff,0,45,Math.PI/7,.55,1);
flashlight.position.set(0,1.4,0);
flashlight.target.position.set(0,1,-1);
camera.add(flashlight);
camera.add(flashlight.target);
scene.add(camera);

const mat=(color,roughness=1)=>new THREE.MeshStandardMaterial({color,roughness});
const grass=mat(0x506d37),dirt=mat(0x514431),wood=mat(0x55412c);
const leaves=mat(0x34512b),rocks=mat(0x77796d),concrete=mat(0x77766c);
const purple=new THREE.MeshStandardMaterial({color:0xa527ff,emissive:0x6b00bb,emissiveIntensity:1.5});

const floor=new THREE.Mesh(new THREE.PlaneGeometry(300,300),grass);
floor.rotation.x=-Math.PI/2;
floor.position.y=-.1;
floor.receiveShadow=true;
scene.add(floor);

const water=new THREE.Mesh(new THREE.PlaneGeometry(100,70),mat(0x315e78,.3));
water.rotation.x=-Math.PI/2;
water.position.set(0,-.04,-95);
scene.add(water);

function path(x,z,w,d){
 const p=new THREE.Mesh(new THREE.PlaneGeometry(w,d),dirt);
 p.rotation.x=-Math.PI/2;
 p.position.set(x,-.045,z);
 scene.add(p);
}
path(0,-10,5,75);
path(-15,-30,38,4);
path(17,-53,40,4);
path(-25,4,4,50);

function tree(x,z,s=1){
 const g=new THREE.Group();
 const trunk=new THREE.Mesh(new THREE.CylinderGeometry(.25*s,.42*s,3*s,7),wood);
 trunk.position.y=1.5*s;
 g.add(trunk);
 for(let i=0;i<3;i++){
  const crown=new THREE.Mesh(new THREE.ConeGeometry((1.7-i*.25)*s,2.5*s,7),leaves);
  crown.position.y=(3+i*.7)*s;
  g.add(crown);
 }
 g.position.set(x,0,z);
 scene.add(g);
}

for(let i=0;i<250;i++){
 const x=(Math.random()-.5)*250,z=(Math.random()-.5)*220;
 if(Math.hypot(x,z)<12)continue;
 if(z < -65 && Math.abs(x)<48)continue;
 tree(x,z,.65+Math.random()*.8);
}

for(let i=0;i<90;i++){
 const r=new THREE.Mesh(new THREE.DodecahedronGeometry(.4+Math.random()*1.1,0),rocks);
 r.position.set((Math.random()-.5)*230,.35,(Math.random()-.5)*210);
 r.rotation.set(Math.random(),Math.random(),Math.random());
 scene.add(r);
}

function building(x,z,w,h,d){
 const g=new THREE.Group();
 const body=new THREE.Mesh(new THREE.BoxGeometry(w,h,d),concrete);
 body.position.y=h/2;
 g.add(body);
 const roof=new THREE.Mesh(new THREE.ConeGeometry(w*.78,1.6,4),mat(0x20221f));
 roof.position.y=h+.8;
 roof.rotation.y=Math.PI/4;
 g.add(roof);
 const door=new THREE.Mesh(new THREE.BoxGeometry(1,1.8,.12),wood);
 door.position.set(0,.9,d/2+.07);
 g.add(door);
 g.position.set(x,0,z);
 scene.add(g);
}
building(-18,-35,10,5,9);
building(30,-53,12,6,11);

const cave=new THREE.Mesh(new THREE.TorusGeometry(4,1.2,8,12,Math.PI),rocks);
cave.rotation.z=Math.PI;
cave.position.set(-35,2,-45);
scene.add(cave);
tree(37,-7,2.1);

function corpse(x,z,color,rotation){
 const g=new THREE.Group(),skin=mat(color);
 const body=new THREE.Mesh(new THREE.SphereGeometry(.48,12,9),skin);
 body.scale.set(1.6,.55,.8);
 body.position.y=.35;
 g.add(body);
 const head=new THREE.Mesh(new THREE.SphereGeometry(.32,12,9),skin);
 head.position.set(-.7,.36,0);
 g.add(head);
 for(const side of [-1,1]){
  const limb=new THREE.Mesh(new THREE.CylinderGeometry(.08,.12,.75,6),skin);
  limb.position.set(.3,.23,side*.4);
  limb.rotation.z=side*.9;
  g.add(limb);
 }
 g.position.set(x,0,z);
 g.rotation.y=rotation;
 scene.add(g);
}
corpse(-8,-24,0x4cba42,.4);
corpse(24,-25,0xd9b83c,1.8);
corpse(-29,-8,0xe3a2b4,2.3);

function tubby(color,antenna,scary=false){
 const g=new THREE.Group(),skin=mat(color);
 const body=new THREE.Mesh(new THREE.SphereGeometry(.65,16,12),skin);
 body.scale.set(.9,1.2,.72);
 body.position.y=1.08;
 g.add(body);
 const head=new THREE.Mesh(new THREE.SphereGeometry(.45,16,12),skin);
 head.position.y=2;
 g.add(head);
 const face=new THREE.Mesh(new THREE.SphereGeometry(.3,12,9),mat(scary?0x665b52:0xcbb99c));
 face.scale.set(1,.72,.4);
 face.position.set(0,1.98,.36);
 g.add(face);
 for(const x of [-.13,.13]){
  const eye=new THREE.Mesh(new THREE.SphereGeometry(.05,8,8),mat(0x151515));
  eye.position.set(x,2.04,.48);
  g.add(eye);
 }
 const mouth=new THREE.Mesh(new THREE.BoxGeometry(.15,.035,.025),mat(0x30211b));
 mouth.position.set(0,1.87,.49);
 g.add(mouth);
 if(antenna===0){
  const a=new THREE.Mesh(new THREE.TorusGeometry(.16,.05,7,12),skin);
  a.position.y=2.58;g.add(a);
 }else if(antenna===1){
  const a=new THREE.Mesh(new THREE.ConeGeometry(.12,.5,8),skin);
  a.position.y=2.67;g.add(a);
 }else if(antenna===2){
  const a=new THREE.Mesh(new THREE.BoxGeometry(.12,.48,.12),skin);
  a.position.y=2.62;g.add(a);
 }else{
  const a=new THREE.Mesh(new THREE.SphereGeometry(.13,8,8),skin);
  a.position.y=2.6;g.add(a);
 }
 for(const x of [-.38,.38]){
  const arm=new THREE.Mesh(new THREE.CapsuleGeometry(.12,.48,3,6),skin);
  arm.position.set(x,1.15,0);g.add(arm);
 }
 for(const x of [-.23,.23]){
  const leg=new THREE.Mesh(new THREE.CapsuleGeometry(.13,.42,3,6),skin);
  leg.position.set(x,.32,0);g.add(leg);
 }
 return g;
}

const characters=[];
[
 {color:0x42a5e8,antenna:0,x:-24,z:8},
 {color:0x4fbb42,antenna:2,x:27,z:22},
 {color:0xd9bd3b,antenna:1,x:43,z:-27}
].forEach((c,i)=>{
 const obj=tubby(c.color,c.antenna);
 obj.position.set(c.x,0,c.z);
 scene.add(obj);
 characters.push({...c,obj,index:i});
});

const enemy=tubby(0x302e32,3,true);
enemy.scale.set(1.2,1.2,1.2);
enemy.position.set(0,0,-48);
scene.add(enemy);

const pickups=[];
for(let i=0;i<10;i++){
 const g=new THREE.Group();
 const container=new THREE.Mesh(new THREE.CylinderGeometry(.29,.23,.55,12),mat(0x522474,.3));
 container.position.y=.28;
 g.add(container);
 const custard=new THREE.Mesh(new THREE.SphereGeometry(.23,12,10),purple);
 custard.scale.y=.42;
 custard.position.y=.58;
 g.add(custard);
 const glow=new THREE.PointLight(0x9c20ff,1.2,4);
 glow.position.y=.7;
 g.add(glow);
 let x,z;
 do{
  x=(Math.random()-.5)*145;
  z=(Math.random()-.5)*125;
 }while(Math.hypot(x,z)<15||z < -82);
 g.position.set(x,0,z);
 scene.add(g);
 pickups.push({obj:g,found:false});
}

let yaw=0,pitch=0,collected=0,battery=100,elapsed=0;
let lightOn=true,paused=false,ended=false,sprint=false;
let lastTime=performance.now();

const keys={};

addEventListener("keydown",e=>{keys[e.code]=true;});
addEventListener("keyup",e=>{keys[e.code]=false;});

document.querySelectorAll("[data-key]").forEach(b=>{
 const k=b.dataset.key;
 b.addEventListener("pointerdown",e=>{e.preventDefault();keys[k]=true;});
 ["pointerup","pointercancel","pointerleave"].forEach(ev=>
  b.addEventListener(ev,()=>keys[k]=false)
 );
});

document.getElementById("flash").onclick=()=>{
 lightOn=!lightOn;
 flashlight.intensity=lightOn&&battery>0?3.5:0;
};

document.getElementById("sprint").addEventListener("pointerdown",()=>sprint=true);
["pointerup","pointercancel","pointerleave"].forEach(ev=>
 document.getElementById("sprint").addEventListener(ev,()=>sprint=false)
);

document.getElementById("pause").onclick=()=>{
 if(ended)return;
 paused=!paused;
 document.getElementById("message").textContent=paused?"PARTIDA EN PAUSA":"";
};

let lastTouch=null;
renderer.domElement.addEventListener("touchstart",e=>{
 const t=e.touches[0];
 if(t.clientX>innerWidth*.42)lastTouch={x:t.clientX,y:t.clientY};
},{passive:true});

renderer.domElement.addEventListener("touchmove",e=>{
 if(!lastTouch)return;
 const t=e.touches[0];
 yaw-=(t.clientX-lastTouch.x)*.006;
 pitch-=(t.clientY-lastTouch.y)*.006;
 pitch=THREE.MathUtils.clamp(pitch,-1.25,1.25);
 lastTouch={x:t.clientX,y:t.clientY};
},{passive:true});

renderer.domElement.addEventListener("touchend",()=>lastTouch=null);

renderer.domElement.addEventListener("click",()=>{
 if(!paused&&!ended)renderer.domElement.requestPointerLock?.();
});

document.addEventListener("mousemove",e=>{
 if(document.pointerLockElement===renderer.domElement){
  yaw-=e.movementX*.002;
  pitch-=e.movementY*.002;
  pitch=THREE.MathUtils.clamp(pitch,-1.25,1.25);
 }
});

function applyEnvironment(mode){
 let sky,fog,intensity;
 if(mode==="day"){sky=0x91b4c9;fog=0x91b4c9;intensity=1.8;}
 else if(mode==="sunset"){sky=0xe19b70;fog=0xc57e65;intensity=.9;}
 else{sky=0x080e1c;fog=0x101426;intensity=.18;}
 scene.background.setHex(sky);
 scene.fog.color.setHex(fog);
 sun.intensity=intensity;
 hemi.intensity=mode==="night"?.3:1.4;
}

function finish(victory){
 if(ended)return;
 ended=true;
 document.getElementById("message").innerHTML=victory
  ?"¡VICTORIA!<br>Encontraste todas las papillas."
  :"¡TINKY WINKY TE ENCONTRÓ!<br>Recarga la página para jugar otra vez.";
}

function getLocation(){
 const x=camera.position.x,z=camera.position.z;
 if(z < -75)return "Playa";
 if(x < -27&&z < -30)return "Cueva";
 if(Math.abs(x+18)<12&&Math.abs(z+35)<12)return "Casa";
 if(Math.hypot(x-37,z+7)<12)return "Árbol";
 if(Math.abs(x)<18&&Math.abs(z)<20)return "Bosque central";
 return "Bosque";
}

function animate(now){
 requestAnimationFrame(animate);
 const dt=Math.min((now-lastTime)/1000,.05);
 lastTime=now;

 if(!paused&&!ended){
  elapsed+=dt;
  const cycle=(elapsed%240)/240;
  const mode=cycle<.48?"day":cycle<.68?"sunset":"night";
  applyEnvironment(mode);

  document.getElementById("time").textContent=
   mode==="day"?"Día":mode==="sunset"?"Atardecer":"Noche";

  if(lightOn&&battery>0){
   flashlight.intensity=mode==="night"?4:2;
   battery-=dt*.12;
  }else{
   flashlight.intensity=0;
   battery=Math.min(100,battery+dt*.025);
  }
  if(battery<=0){battery=0;flashlight.intensity=0;}
  document.getElementById("battery").textContent=Math.floor(battery);

  camera.rotation.order="YXZ";
  camera.rotation.y=yaw;
  camera.rotation.x=pitch;

  let f=0,s=0;
  if(keys.KeyW||keys.ArrowUp)f++;
  if(keys.KeyS||keys.ArrowDown)f--;
  if(keys.KeyA||keys.ArrowLeft)s--;
  if(keys.KeyD||keys.ArrowRight)s++;

  const dir=new THREE.Vector3(s,0,-f);
  if(dir.length()>0)dir.normalize();
  dir.applyAxisAngle(new THREE.Vector3(0,1,0),yaw);
  const speed=sprint?8:4.3;
  camera.position.x+=dir.x*speed*dt;
  camera.position.z+=dir.z*speed*dt;
  camera.position.x=THREE.MathUtils.clamp(camera.position.x,-140,140);
  camera.position.z=THREE.MathUtils.clamp(camera.position.z,-125,125);
  camera.position.y=1.7;

  for(const p of pickups){
   if(p.found)continue;
   p.obj.rotation.y+=dt*.65;
   p.obj.position.y=Math.sin(elapsed*2+p.obj.position.x)*.08;
   if(camera.position.distanceTo(p.obj.position)<1.8){
    p.found=true;
    scene.remove(p.obj);
    collected++;
    document.getElementById("count").textContent=collected;
    document.getElementById("message").textContent=
      collected===10?"¡Tienes las diez! Regresa al punto inicial.":"¡Papilla recogida!";
   }
  }

  characters.forEach(c=>{
   const t=elapsed*.24+c.index*2.2;
   c.obj.position.x=c.x+Math.sin(t)*4;
   c.obj.position.z=c.z+Math.cos(t)*4;
   c.obj.rotation.y=Math.sin(t);
  });

  const target=new THREE.Vector3(camera.position.x,0,camera.position.z);
  const distance=enemy.position.distanceTo(target);
  const chaseSpeed=mode==="night"?3.3:1.65;

  if(distance>1.7){
   const v=target.clone().sub(enemy.position).normalize();
   enemy.position.addScaledVector(v,chaseSpeed*dt);
   enemy.rotation.y=Math.atan2(v.x,v.z);
  }

  document.getElementById("danger").textContent=
   distance<12?"¡PELIGRO!":distance<30?"Se acerca":"Lejos";
  document.getElementById("place").textContent=getLocation();

  if(distance<1.7)finish(false);
  if(collected===10&&Math.hypot(camera.position.x,camera.position.z)<7)finish(true);
 }

 renderer.render(scene,camera);
}

addEventListener("resize",()=>{
 camera.aspect=innerWidth/innerHeight;
 camera.updateProjectionMatrix();
 renderer.setSize(innerWidth,innerHeight);
});

requestAnimationFrame(animate);
})();
</script>
</body>
</html>

Game Source: Slendytubbies 1 - Juego 3D

Creator: MagicKnight13

Libraries: three

Complexity: complex (457 lines, 14.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: slendytubbies-1-juego-3d-magicknight13" to link back to the original. Then publish at arcadelab.ai/publish.