Slendytubbies 1 - Juego 3D
by MagicKnight13457 lines14.0 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>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.