Slendytubbies: Bosque Oscuro 3D
by MagicKnight13740 lines20.3 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: Bosque Oscuro 3D</title>
<style>
*{box-sizing:border-box}
html,body{margin:0;width:100%;height:100%;overflow:hidden;background:#020603;color:white;font-family:Arial,sans-serif;touch-action:none}
canvas{display:block}
#hud{position:fixed;inset:0;pointer-events:none;text-shadow:0 2px 5px #000}
#title{position:absolute;top:12px;left:12px;font-size:clamp(15px,3vw,23px);font-weight:900;letter-spacing:1px}
#stats{position:absolute;top:46px;left:12px;line-height:1.65;font-size:14px}
#hint{position:absolute;top:15%;left:50%;transform:translateX(-50%);padding:10px 15px;background:#020603b8;border:1px solid #a7d5a344;border-radius:10px;text-align:center;max-width:90vw;font-size:14px}
#cross{position:absolute;top:50%;left:50%;width:7px;height:7px;border:1px solid #ffffff99;border-radius:50%;transform:translate(-50%,-50%)}
#overlay{position:fixed;inset:0;display:flex;align-items:center;justify-content:center;background:linear-gradient(#010402ed,#07120bf2);z-index:10;padding:15px}
.panel{width:min(520px,95vw);max-height:92vh;overflow:auto;padding:24px;background:#0b1710f5;border:1px solid #789d7b77;border-radius:18px;text-align:center;box-shadow:0 15px 60px #000}
h1{color:#c4edc4;font-size:clamp(25px,6vw,38px);margin:0 0 12px}
p{line-height:1.5;color:#d0ded2}
button{border:1px solid #ffffff44;border-radius:12px;padding:12px 16px;background:#b8e0b5;color:#102012;font-weight:bold;font-size:15px;margin:3px}
.small{font-size:12px;color:#9caf9f}
#mobile{position:fixed;bottom:12px;left:0;right:0;display:none;justify-content:space-between;align-items:end;padding:0 12px;z-index:3;pointer-events:none}
.pad{display:grid;grid-template-columns:repeat(3,45px);grid-template-rows:repeat(3,45px);gap:4px;pointer-events:auto}
.pad button,.actions button{background:#102018d9;color:white;border:1px solid #ffffff44;touch-action:none}
.pad button{padding:0;font-size:19px}
.empty{visibility:hidden}
.actions{display:flex;flex-direction:column;gap:6px;pointer-events:auto}
.actions button{width:76px;height:48px;padding:3px;font-size:12px}
@media(pointer:coarse){#mobile{display:flex}}
</style>
</head>
<body>
<div id="hud">
<div id="title">SLENDYTUBBIES: BOSQUE OSCURO</div>
<div id="stats">
Recipientes: <span id="count">0</span>/10<br>
Linterna: <span id="battery">100</span>%<br>
<span id="state">Esperando...</span>
</div>
<div id="hint">Encuentra los 10 recipientes y evita a la criatura.</div>
<div id="cross"></div>
</div>
<div id="mobile">
<div class="pad">
<button class="empty"></button><button data-key="w">▲</button><button class="empty"></button>
<button data-key="a">◀</button><button data-key="s">▼</button><button data-key="d">▶</button>
<button class="empty"></button><button data-key="run">⚡</button><button class="empty"></button>
</div>
<div class="actions">
<button id="lightButton">🔦 LINTERNA</button>
<button id="runButton">CORRER</button>
</div>
</div>
<div id="overlay">
<div class="panel">
<h1>SLENDYTUBBIES<br>BOSQUE OSCURO 3D</h1>
<p>La noche cubre un bosque abandonado. Encuentra los <b>10 recipientes</b> y regresa al claro inicial para escapar.</p>
<p>🌲 Bosque tridimensional<br>
🌫️ Niebla y noche oscura<br>
🔦 Linterna con batería<br>
👾 Criatura que te persigue<br>
🥫 Objetos coleccionables<br>
📱 Controles táctiles</p>
<p><b>PC:</b> WASD para moverte, mouse para mirar, Shift para correr y F para la linterna.</p>
<p><b>Android:</b> usa los botones y desliza el dedo sobre la parte derecha de la pantalla para mirar.</p>
<button id="startButton">COMENZAR PARTIDA</button>
<p class="small">Versión experimental. Los gráficos y modelos se generan mediante código.</p>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/three@0.160.0/build/three.min.js"></script>
<script>
(() => {
"use strict";
const $ = id => document.getElementById(id);
if (!window.THREE) {
$("hint").textContent = "No se pudo cargar Three.js. Comprueba tu conexión a Internet.";
$("startButton").disabled = true;
return;
}
/* =========================
MOTOR 3D Y ESCENA
========================= */
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x050a08);
scene.fog = new THREE.FogExp2(0x09130e, 0.027);
const camera = new THREE.PerspectiveCamera(
75, innerWidth / innerHeight, 0.08, 180
);
camera.position.set(0, 1.7, 0);
const renderer = new THREE.WebGLRenderer({
antialias: true,
powerPreference: "high-performance"
});
renderer.setSize(innerWidth, innerHeight);
renderer.setPixelRatio(Math.min(devicePixelRatio, 1.5));
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
renderer.outputColorSpace = THREE.SRGBColorSpace;
renderer.toneMapping = THREE.ACESFilmicToneMapping;
renderer.toneMappingExposure = 1.05;
document.body.appendChild(renderer.domElement);
scene.add(new THREE.HemisphereLight(0x8ca9bd, 0x10190e, 0.55));
const moon = new THREE.DirectionalLight(0x9ab9d7, 0.8);
moon.position.set(-15, 28, -12);
moon.castShadow = true;
moon.shadow.mapSize.set(1024, 1024);
moon.shadow.camera.left = -35;
moon.shadow.camera.right = 35;
moon.shadow.camera.top = 35;
moon.shadow.camera.bottom = -35;
scene.add(moon);
/* =========================
TEXTURAS PROCEDURALES
========================= */
function makeTexture(kind, base, detail, size=256) {
const c = document.createElement("canvas");
c.width = c.height = size;
const g = c.getContext("2d");
g.fillStyle = base;
g.fillRect(0,0,size,size);
if (kind === "ground") {
for(let i=0;i<2400;i++){
const x=Math.random()*size,y=Math.random()*size;
g.fillStyle=Math.random()>.5?detail:"#18251a";
g.globalAlpha=Math.random()*.45;
g.beginPath();
g.ellipse(x,y,Math.random()*3+1,Math.random()*2+1,Math.random()*3,0,Math.PI*2);
g.fill();
}
g.globalAlpha=1;
for(let i=0;i<80;i++){
g.fillStyle=Math.random()>.5?"#443d2c":"#292f20";
g.save();
g.translate(Math.random()*size,Math.random()*size);
g.rotate(Math.random()*Math.PI);
g.fillRect(0,0,Math.random()*9+2,Math.random()*3+1);
g.restore();
}
}
if (kind === "bark") {
for(let i=0;i<160;i++){
g.fillStyle=Math.random()>.5?detail:"#30241a";
g.globalAlpha=Math.random()*.55;
const x=Math.random()*size;
g.fillRect(x,0,Math.random()*5+1,size);
}
g.globalAlpha=1;
for(let i=0;i<35;i++){
g.strokeStyle="#1c1712";
g.globalAlpha=.6;
g.lineWidth=1+Math.random()*2;
g.beginPath();
const x=Math.random()*size;
g.moveTo(x,0);
g.lineTo(x+Math.random()*18-9,size);
g.stroke();
}
g.globalAlpha=1;
}
if (kind === "leaves") {
for(let i=0;i<500;i++){
g.fillStyle=Math.random()>.5?detail:"#101e13";
g.globalAlpha=.25+Math.random()*.55;
g.beginPath();
g.ellipse(Math.random()*size,Math.random()*size,
Math.random()*12+2,Math.random()*6+1,Math.random()*3,0,Math.PI*2);
g.fill();
}
g.globalAlpha=1;
}
const t=new THREE.CanvasTexture(c);
t.wrapS=t.wrapT=THREE.RepeatWrapping;
t.colorSpace=THREE.SRGBColorSpace;
t.anisotropy=Math.min(4,renderer.capabilities.getMaxAnisotropy());
return t;
}
const groundTexture=makeTexture("ground","#283329","#46503a");
groundTexture.repeat.set(18,18);
const barkTexture=makeTexture("bark","#51402d","#756044");
barkTexture.wrapS=barkTexture.wrapT=THREE.RepeatWrapping;
const leafTexture=makeTexture("leaves","#18291a","#30472a");
/* =========================
SUELO Y VEGETACIÓN
========================= */
const ground=new THREE.Mesh(
new THREE.PlaneGeometry(120,120),
new THREE.MeshStandardMaterial({
map:groundTexture,roughness:1
})
);
ground.rotation.x=-Math.PI/2;
ground.position.y=-0.08;
ground.receiveShadow=true;
scene.add(ground);
const barkMat=new THREE.MeshStandardMaterial({
map:barkTexture,roughness:1
});
const leafMat=new THREE.MeshStandardMaterial({
map:leafTexture,color:0x9ab18c,roughness:1
});
const grassMat=new THREE.MeshStandardMaterial({
color:0x3d5735,roughness:1
});
const rockMat=new THREE.MeshStandardMaterial({
color:0x303b34,roughness:1
});
const obstacles=[];
const worldObjects=[];
const rand=(a,b)=>a+Math.random()*(b-a);
function addTree(x,z,scale=1){
const group=new THREE.Group();
group.position.set(x,0,z);
group.scale.setScalar(scale);
const trunk=new THREE.Mesh(
new THREE.CylinderGeometry(.25,.42,5.2,7),
barkMat
);
trunk.position.y=2.6;
trunk.castShadow=true;
trunk.receiveShadow=true;
group.add(trunk);
const crown1=new THREE.Mesh(
new THREE.ConeGeometry(2.3,4.4,7),
leafMat
);
crown1.position.y=5.3;
crown1.castShadow=true;
group.add(crown1);
const crown2=new THREE.Mesh(
new THREE.ConeGeometry(1.8,3.7,7),
leafMat
);
crown2.position.y=7;
crown2.castShadow=true;
group.add(crown2);
scene.add(group);
obstacles.push({x,z,r:.65*scale});
worldObjects.push(group);
}
for(let i=0;i<125;i++){
const x=rand(-48,48),z=rand(-48,48);
if(Math.hypot(x,z)<5)continue;
if(Math.hypot(x-0,z+2)<4)continue;
addTree(x,z,rand(.7,1.5));
}
/* Hierba y piedras decorativas */
for(let i=0;i<260;i++){
const x=rand(-48,48),z=rand(-48,48);
if(Math.hypot(x,z)<2.5)continue;
const group=new THREE.Group();
group.position.set(x,0,z);
for(let j=0;j<3;j++){
const blade=new THREE.Mesh(
new THREE.ConeGeometry(.045,rand(.25,.55),4),
grassMat
);
blade.position.set(rand(-.16,.16),.17,rand(-.16,.16));
blade.rotation.z=rand(-.3,.3);
group.add(blade);
}
scene.add(group);
}
for(let i=0;i<65;i++){
const x=rand(-45,45),z=rand(-45,45);
const rock=new THREE.Mesh(
new THREE.DodecahedronGeometry(rand(.15,.5),0),
rockMat
);
rock.position.set(x,.12,z);
rock.scale.y=rand(.4,.9);
rock.rotation.set(rand(0,2),rand(0,2),rand(0,2));
rock.castShadow=true;
scene.add(rock);
}
/* =========================
LINTERNA
========================= */
const flashlight=new THREE.SpotLight(
0xe1f0d2, 45, 30, Math.PI/7, .45, 1.4
);
flashlight.position.set(0,0,0);
flashlight.target.position.set(0,0,-1);
camera.add(flashlight);
camera.add(flashlight.target);
scene.add(camera);
let lightOn=true;
let battery=100;
function toggleLight(){
if(battery<=0)return;
lightOn=!lightOn;
flashlight.visible=lightOn;
}
/* =========================
RECIPIENTES COLECCIONABLES
========================= */
const items=[];
const itemMat=new THREE.MeshStandardMaterial({
color:0xbceca3,
emissive:0x3b8e28,
emissiveIntensity:1.6,
roughness:.25,
metalness:.05
});
const capMat=new THREE.MeshStandardMaterial({
color:0x547d43,roughness:.5
});
function createItem(x,z){
const group=new THREE.Group();
const body=new THREE.Mesh(
new THREE.CapsuleGeometry(.24,.42,5,10),
itemMat
);
body.position.y=.45;
group.add(body);
const cap=new THREE.Mesh(
new THREE.CylinderGeometry(.16,.19,.13,10),
capMat
);
cap.position.y=.76;
group.add(cap);
const glow=new THREE.PointLight(0x8bff67,1.5,3);
glow.position.y=.6;
group.add(glow);
group.position.set(x,0,z);
group.userData={
collected:false,
phase:Math.random()*6
};
scene.add(group);
items.push(group);
}
while(items.length<10){
const x=rand(-29,29),z=rand(-29,29);
if(Math.hypot(x,z)<7)continue;
if(items.some(o=>Math.hypot(o.position.x-x,o.position.z-z)<5))continue;
createItem(x,z);
}
/* =========================
CRIATURA ORIGINAL 3D
========================= */
const monster=new THREE.Group();
scene.add(monster);
const suitMat=new THREE.MeshStandardMaterial({
color:0x682d3b,roughness:.82
});
const darkMat=new THREE.MeshStandardMaterial({
color:0x20171b,roughness:1
});
const faceMat=new THREE.MeshStandardMaterial({
color:0x4b2930,roughness:.9
});
const eyeMat=new THREE.MeshStandardMaterial({
color:0xffdca0,
emissive:0xff5b24,
emissiveIntensity:3
});
const torso=new THREE.Mesh(
new THREE.CapsuleGeometry(.43,.8,6,10),suitMat
);
torso.position.y=1.22;
torso.castShadow=true;
monster.add(torso);
const head=new THREE.Mesh(
new THREE.SphereGeometry(.39,16,12),faceMat
);
head.position.y=2.02;
head.scale.set(1,.92,.9);
head.castShadow=true;
monster.add(head);
for(const side of [-1,1]){
const eye=new THREE.Mesh(
new THREE.SphereGeometry(.055,10,8),eyeMat
);
eye.position.set(side*.14,2.08,-.33);
monster.add(eye);
}
const antenna=new THREE.Mesh(
new THREE.CylinderGeometry(.035,.06,.48,7),
suitMat
);
antenna.position.y=2.45;
monster.add(antenna);
const antennaBall=new THREE.Mesh(
new THREE.SphereGeometry(.11,10,8),
suitMat
);
antennaBall.position.y=2.7;
monster.add(antennaBall);
const limbs=[];
function makeLimb(x,y,length,width){
const pivot=new THREE.Group();
pivot.position.set(x,y,0);
const mesh=new THREE.Mesh(
new THREE.CapsuleGeometry(width,length,4,8),
suitMat
);
mesh.position.y=-length*.35;
mesh.castShadow=true;
pivot.add(mesh);
monster.add(pivot);
limbs.push(pivot);
return pivot;
}
makeLimb(-.48,1.55,.75,.14);
makeLimb(.48,1.55,.75,.14);
makeLimb(-.23,.72,.7,.17);
makeLimb(.23,.72,.7,.17);
const mouth=new THREE.Mesh(
new THREE.BoxGeometry(.2,.045,.035),
darkMat
);
mouth.position.set(0,1.91,-.36);
monster.add(mouth);
monster.position.set(18,0,22);
/* =========================
JUGADOR Y CONTROLES
========================= */
const player={
x:0,z:0,yaw:0,pitch:0,
speed:3.2,health:100
};
const keys={};
let started=false,ended=false,found=0;
let lastTime=performance.now();
let lastMessage=0;
let walkingTime=0;
addEventListener("keydown",e=>{
keys[e.key.toLowerCase()]=true;
if(e.key.toLowerCase()==="f")toggleLight();
if(["arrowup","arrowdown","arrowleft","arrowright"," "].includes(e.key.toLowerCase()))e.preventDefault();
});
addEventListener("keyup",e=>keys[e.key.toLowerCase()]=false);
document.querySelectorAll("[data-key]").forEach(btn=>{
const key=btn.dataset.key;
btn.addEventListener("pointerdown",e=>{
e.preventDefault();keys[key]=true;
});
["pointerup","pointercancel","pointerleave"].forEach(ev=>{
btn.addEventListener(ev,e=>{
e.preventDefault();keys[key]=false;
});
});
});
$("lightButton").onclick=toggleLight;
$("runButton").addEventListener("pointerdown",()=>keys.run=true);
["pointerup","pointercancel","pointerleave"].forEach(ev=>
$("runButton").addEventListener(ev,()=>keys.run=false)
);
let looking=false,lastX=0,lastY=0;
renderer.domElement.addEventListener("pointerdown",e=>{
if(!started||ended)return;
if(e.clientX>innerWidth*.35){
looking=true;
lastX=e.clientX;
lastY=e.clientY;
renderer.domElement.setPointerCapture(e.pointerId);
}
});
renderer.domElement.addEventListener("pointermove",e=>{
if(!looking)return;
player.yaw-=(e.clientX-lastX)*.004;
player.pitch-=(e.clientY-lastY)*.003;
player.pitch=Math.max(-1.15,Math.min(1.15,player.pitch));
lastX=e.clientX;
lastY=e.clientY;
});
["pointerup","pointercancel"].forEach(ev=>
renderer.domElement.addEventListener(ev,()=>looking=false)
);
document.addEventListener("mousemove",e=>{
if(document.pointerLockElement===renderer.domElement){
player.yaw-=e.movementX*.002;
player.pitch-=e.movementY*.002;
player.pitch=Math.max(-1.15,Math.min(1.15,player.pitch));
}
});
/* =========================
MENSAJES Y PARTIDA
========================= */
function say(text){
$("hint").textContent=text;
lastMessage=performance.now()+2500;
}
$("startButton").onclick=()=>{
started=true;
$("overlay").style.display="none";
say("Encuentra los 10 recipientes. ¡No hagas ruido!");
if(matchMedia("(pointer:fine)").matches&&renderer.domElement.requestPointerLock){
renderer.domElement.requestPointerLock();
}
};
function finish(win){
if(ended)return;
ended=true;
document.exitPointerLock?.();
$("overlay").style.display="flex";
$("overlay").innerHTML=`
<div class="panel">
<h1>${win?"¡ESCAPASTE!":"TE ATRAPARON"}</h1>
<p>${win
?"Has encontrado los 10 recipientes y regresado al claro."
:"La criatura te encontró en el bosque. Puedes intentarlo otra vez."}</p>
<p>Recipientes: ${found}/10</p>
<button onclick="location.reload()">VOLVER A JUGAR</button>
</div>`;
}
/* =========================
ACTUALIZACIÓN DEL JUEGO
========================= */
const clock=new THREE.Clock();
function update(dt){
if(!started||ended)return;
const forward=(keys.w||keys.arrowup?1:0)-
(keys.s||keys.arrowdown?1:0);
const side=(keys.d||keys.arrowright?1:0)-
(keys.a||keys.arrowleft?1:0);
const running=keys.shift||keys.run;
const speed=(running?5.3:3.2)*dt;
const oldX=player.x,oldZ=player.z;
player.x+=Math.sin(player.yaw)*forward*speed+
Math.cos(player.yaw)*side*speed;
player.z-=Math.cos(player.yaw)*forward*speed-
Math.sin(player.yaw)*side*speed;
player.x=Math.max(-53,Math.min(53,player.x));
player.z=Math.max(-53,Math.min(53,player.z));
// Colisión sencilla con los troncos
for(const tree of obstacles){
const dx=player.x-tree.x,dz=player.z-tree.z;
const dist=Math.hypot(dx,dz);
if(dist<tree.r+.35&&dist>0){
player.x=tree.x/dx*(tree.r+.36)+player.x;
player.z=tree.z/dz*(tree.r+.36)+player.z;
if(!Number.isFinite(player.x)||!Number.isFinite(player.z)){
player.x=oldX;player.z=oldZ;
}
}
}
camera.position.set(player.x,1.7,player.z);
camera.rotation.order="YXZ";
camera.rotation.y=player.yaw;
camera.rotation.x=player.pitch;
walkingTime+=dt*(running?13:8)*(Math.abs(forward)+Math.abs(side));
camera.position.y=1.7+Math.sin(walkingTime)*.025*
Math.min(1,Math.abs(forward)+Math.abs(side));
if(lightOn&&battery>0){
battery=Math.max(0,battery-dt*.45);
}
if(battery<=0){
lightOn=false;
flashlight.visible=false;
}
$("battery").textContent=Math.ceil(battery);
// Animación de los recipientes
for(const item of items){
if(item.userData.collected)continue;
item.rotation.y+=dt*.8;
item.position.y=.1+Math.sin(clock.elapsedTime*2+
item.userData.phase)*.08;
if(Math.hypot(item.position.x-player.x,
item.position.z-player.z)<1.35){
item.userData.collected=true;
scene.remove(item);
found++;
$("count").textContent=found;
if(found===10){
say("¡Los encontraste todos! Regresa al claro inicial.");
}else{
say("Recipiente encontrado: "+found+"/10");
}
}
}
// La criatura persigue al jugador
const dx=player.x-monster.position.x;
const dz=player.z-monster.position.z;
const distance=Math.hypot(dx,dz);
const monsterSpeed=found>=7?2.15:found>=4?1.65:1.2;
if(distance>.05){
monster.position.x+=(dx/distance)*monsterSpeed*dt;
monster.position.z+=(dz/distance)*monsterSpeed*dt;
monster.rotation.y=Math.atan2(dx,dz);
}
// Animación de caminar
const walk=Math.sin(clock.elapsedTime*7);
limbs[0].rotation.x=walk*.55;
limbs[1].rotation.x=-walk*.55;
limbs[2].rotation.x=-walk*.6;
limbs[3].rotation.x=walk*.6;
torso.position.y=1.22+Math.abs(walk)*.035;
head.rotation.z=Math.sin(clock.elapsedTime*1.5)*.05;
// Daño al estar demasiado cerca
if(distance<1.15){
player.health-=dt*28;
if(player.health<=0)finish(false);
}
// La salida está cerca del punto inicial
if(found===10&&Math.hypot(player.x,player.z+2)<2.3){
finish(true);
}
$("state").textContent=distance<8
?"¡LA CRIATURA ESTÁ CERCA!"
:lightOn?"Explorando":"Linterna apagada";
if(distance<8&&performance.now()>lastMessage){
$("hint").textContent="Escuchas pasos entre los árboles...";
}else if(performance.now()>lastMessage){
$("hint").textContent=found===10
?"Regresa al claro inicial para escapar."
:"Encuentra los 10 recipientes y evita a la criatura.";
}
}
/* =========================
BUCLE PRINCIPAL
========================= */
function animate(){
requestAnimationFrame(animate);
const dt=Math.min(clock.getDelta(),.05);
update(dt);
// Oscurecimiento progresivo al quedarse sin linterna
renderer.toneMappingExposure=lightOn&&battery>0?1.05:.62;
renderer.render(scene,camera);
}
animate();
addEventListener("resize",()=>{
camera.aspect=innerWidth/innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(innerWidth,innerHeight);
renderer.setPixelRatio(Math.min(devicePixelRatio,1.5));
});
})();
</script>
</body>
</html>Game Source: Slendytubbies: Bosque Oscuro 3D
Creator: MagicKnight13
Libraries: three
Complexity: complex (740 lines, 20.3 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-bosque-oscuro-3d-magicknight13" to link back to the original. Then publish at arcadelab.ai/publish.