A 3D scene with Three.js and orbit controls, loaded through an import map.
Sí. Entiendo que quieres una vista **3D en tercera persona**, con la cámara siguiendo al dron, como en un juego de acción estilo GTA, no una imagen 2D. Esta versión usa Three.js y controles táctiles básicos; requiere conexión a internet para cargar la biblioteca 3D. Guarda el código como `drone-3d.html` y ábrelo en Chrome.
```html
<!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>Drone 166 — Combate 3D</title>
<style>
* { box-sizing: border-box }
html, body { margin: 0; width: 100%; height: 100%; overflow: hidden; background: #161a1b; touch-action: none }
canvas { display: block }
#hud {
position: fixed; top: 12px; left: 50%; transform: translateX(-50%);
color: #eee; background: #111b; padding: 8px 16px;
font: 12px Arial; text-align: center; border: 1px solid #aaa5;
pointer-events: none; min-width: 190px;
}
#help {
position: fixed; bottom: 10px; left: 50%; transform: translateX(-50%);
color: #eee; background: #111b; padding: 7px 10px;
font: 11px Arial; pointer-events: none; white-space: nowrap;
}
#rotate {
display: none; position: fixed; inset: 0; z-index: 5;
align-items: center; justify-content: center; padding: 30px;
color: white; background: #161a1b; font: 17px Arial; text-align: center;
}
@media (orientation: portrait) { #rotate { display: flex } }
</style>
</head>
<body>
<div id="rotate">Gira el teléfono en horizontal para jugar.</div>
<div id="hud">DRONE 166<br><span id="status">BLANCOS: 5 · ENERGÍA: 100%</span></div>
<div id="help">Arrastra lado izquierdo: mover · Mantén lado derecho: disparar</div>
<script type="module">
import * as THREE from "https://cdn.jsdelivr.net/npm/three@0.160.0/build/three.module.js";
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x89908e);
scene.fog = new THREE.Fog(0x89908e, 35, 145);
const camera = new THREE.PerspectiveCamera(65, innerWidth/innerHeight, .1, 250);
const renderer = new THREE.WebGLRenderer({antialias:true});
renderer.setSize(innerWidth, innerHeight);
renderer.setPixelRatio(Math.min(devicePixelRatio, 2));
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
renderer.toneMapping = THREE.ACESFilmicToneMapping;
renderer.toneMappingExposure = 1.15;
document.body.appendChild(renderer.domElement);
scene.add(new THREE.HemisphereLight(0xdde2df, 0x343633, 2.2));
const sun = new THREE.DirectionalLight(0xffe6c2, 3);
sun.position.set(-25, 40, 15); sun.castShadow = true;
sun.shadow.mapSize.set(2048,2048); scene.add(sun);
const mat = (color, roughness=.9, metalness=.1) =>
new THREE.MeshStandardMaterial({color,roughness,metalness});
const ground = new THREE.Mesh(new THREE.PlaneGeometry(220,220),mat(0x41423d));
ground.rotation.x=-Math.PI/2; ground.receiveShadow=true; scene.add(ground);
// Carretera industrial
const road = new THREE.Mesh(new THREE.PlaneGeometry(24,180),mat(0x303331));
road.rotation.x=-Math.PI/2; road.position.y=.025; road.receiveShadow=true; scene.add(road);
for(let z=-80;z<90;z+=12){
const stripe=new THREE.Mesh(new THREE.BoxGeometry(.22,.04,5),mat(0xa09d8e));
stripe.position.set(0,.06,z);scene.add(stripe);
}
// Edificios, ventanas y chimeneas
for(let i=0;i<42;i++){
const side=i%2?1:-1, z=-85+Math.random()*170;
const w=5+Math.random()*9, h=5+Math.random()*18, d=7+Math.random()*12;
const b=new THREE.Mesh(new THREE.BoxGeometry(w,h,d),
mat([0x424947,0x555b58,0x373d3d,0x686960][i%4]));
b.position.set(side*(17+Math.random()*17),h/2,z);
b.castShadow=true;b.receiveShadow=true;scene.add(b);
const rows=Math.floor(h/3), cols=Math.max(1,Math.floor(w/2.5));
for(let r=1;r<rows;r++) for(let c=0;c<cols;c++){
const win=new THREE.Mesh(new THREE.BoxGeometry(.65,.8,.06),
mat(Math.random()<.22?0xb17d4b:0x202827));
win.position.set(b.position.x-w/2+1+c*2.3,r*2.6,z+d/2+.04);scene.add(win);
}
}
// Escombros
for(let i=0;i<90;i++){
const rock=new THREE.Mesh(new THREE.DodecahedronGeometry(.3+Math.random()*1.2,0),mat(0x55564f));
rock.position.set((Math.random()-.5)*44,.3, (Math.random()-.5)*150);
rock.rotation.set(Math.random()*3,Math.random()*3,Math.random()*3);
rock.castShadow=true;scene.add(rock);
}
// Dron: carcasa, sensores, brazos y cañones
const drone=new THREE.Group(); scene.add(drone);
const shell=new THREE.Mesh(new THREE.SphereGeometry(1,20,14),mat(0x777c78,.55,.45));
shell.scale.set(1.35,.8,1.05);shell.castShadow=true;drone.add(shell);
const plate=new THREE.Mesh(new THREE.BoxGeometry(1.35,.55,.25),mat(0x353b3b,.45,.5));
plate.position.set(0,0,-.9);drone.add(plate);
const label=new THREE.Mesh(new THREE.BoxGeometry(.75,.08,.04),mat(0xd3d0c3));
label.position.set(0,.12,-1.05);drone.add(label);
const red=new THREE.Mesh(new THREE.SphereGeometry(.16,12,8),
new THREE.MeshStandardMaterial({color:0xff1616,emissive:0xff1717,emissiveIntensity:3}));
red.position.set(-.45,.12,-1.12);drone.add(red);
for(const side of [-1,1]){
const arm=new THREE.Mesh(new THREE.BoxGeometry(2.1,.18,.2),mat(0x282e2f,.5,.5));
arm.position.set(side*1.05,0,-.1);drone.add(arm);
const gun=new THREE.Mesh(new THREE.CylinderGeometry(.12,.16,.9,10),mat(0x242929,.45,.5));
gun.rotation.x=Math.PI/2;gun.position.set(side*1.8,-.05,-.75);drone.add(gun);
}
drone.position.set(0,5,8);
// Robots enemigos
const enemies=[];
function makeEnemy(x,z){
const g=new THREE.Group();
const armor=mat(0x414846,.65,.45), dark=mat(0x222727,.55,.55);
const torso=new THREE.Mesh(new THREE.BoxGeometry(.9,1.25,.55),armor);
torso.position.y=1.45;torso.castShadow=true;g.add(torso);
const head=new THREE.Mesh(new THREE.BoxGeometry(.55,.45,.5),mat(0x666b65));
head.position.y=2.35;g.add(head);
const visor=new THREE.Mesh(new THREE.BoxGeometry(.42,.09,.06),
new THREE.MeshStandardMaterial({color:0xff442d,emissive:0xaa1809}));
visor.position.set(0,2.38,-.28);g.add(visor);
for(const s of [-1,1]){
const leg=new THREE.Mesh(new THREE.BoxGeometry(.28,.95,.32),dark);
leg.position.set(s*.27,.47,0);g.add(leg);
const arm=new THREE.Mesh(new THREE.BoxGeometry(.28,.9,.32),armor);
arm.position.set(s*.68,1.5,0);g.add(arm);
}
g.position.set(x,0,z);scene.add(g);
enemies.push({group:g,hp:3,alive:true,cooldown:1+Math.random()*2});
}
for(let i=0;i<5;i++) makeEnemy((Math.random()-.5)*30,-15-Math.random()*55);
// Explosiones sencillas y proyectiles
const bullets=[], enemyShots=[], particles=[];
function shoot(){
const dir=new THREE.Vector3(0,0,-1).applyQuaternion(drone.quaternion).normalize();
const start=drone.position.clone().add(dir.clone().multiplyScalar(2));
const mesh=new THREE.Mesh(new THREE.SphereGeometry(.13,8,6),
new THREE.MeshBasicMaterial({color:0xffd28a}));
mesh.position.copy(start);scene.add(mesh);
bullets.push({mesh,dir,life:2});
}
function enemyShoot(e){
const mesh=new THREE.Mesh(new THREE.SphereGeometry(.12,8,6),
new THREE.MeshBasicMaterial({color:0xff5b32}));
mesh.position.copy(e.group.position).add(new THREE.Vector3(0,1.6,0));
scene.add(mesh);
const target=drone.position.clone();
const dir=target.sub(mesh.position).normalize();
enemyShots.push({mesh,dir,life:3});
}
function explosion(pos,color=0xff8b37){
for(let i=0;i<18;i++){
const m=new THREE.Mesh(new THREE.SphereGeometry(.08+Math.random()*.12,6,5),
new THREE.MeshBasicMaterial({color,transparent:true}));
m.position.copy(pos);scene.add(m);
particles.push({mesh:m,vel:new THREE.Vector3(Math.random()-.5,Math.random(),Math.random()-.5).multiplyScalar(5),life:.7+Math.random()*.7});
}
}
// Controles táctiles y ratón
let joy=null, firing=false, energy=100, score=0, shotTimer=0;
const keys={};
addEventListener("keydown",e=>keys[e.key.toLowerCase()]=true);
addEventListener("keyup",e=>keys[e.key.toLowerCase()]=false);
renderer.domElement.addEventListener("pointerdown",e=>{
renderer.domElement.setPointerCapture(e.pointerId);
if(e.clientX<innerWidth*.45) joy={id:e.pointerId,x:e.clientX,y:e.clientY};
else firing=true;
});
renderer.domElement.addEventListener("pointermove",e=>{
if(joy&&joy.id===e.pointerId){joy.x=e.clientX;joy.y=e.clientY;}
});
function release(){joy=null;firing=false;}
renderer.domElement.addEventListener("pointerup",release);
renderer.domElement.addEventListener("pointercancel",release);
const clock=new THREE.Clock();
function animate(){
requestAnimationFrame(animate);
const dt=Math.min(clock.getDelta(),.04);
const move=new THREE.Vector3();
if(keys.w||keys.arrowup)move.z-=1;
if(keys.s||keys.arrowdown)move.z+=1;
if(keys.a||keys.arrowleft)move.x-=1;
if(keys.d||keys.arrowright)move.x+=1;
if(joy){
const dx=(joy.x-innerWidth*.22)/55, dy=(joy.y-innerHeight*.76)/55;
move.x+=THREE.MathUtils.clamp(dx,-1,1);
move.z+=THREE.MathUtils.clamp(dy,-1,1);
}
if(move.length()>0){
move.normalize();drone.position.addScaledVector(move,9*dt);
drone.rotation.y=Math.atan2(move.x,-move.z);
}
drone.position.x=THREE.MathUtils.clamp(drone.position.x,-24,24);
drone.position.z=THREE.MathUtils.clamp(drone.position.z,-8,34);
shotTimer-=dt;
if(firing&&shotTimer<=0){shoot();shotTimer=.16;}
for(const b of bullets){
b.mesh.position.addScaledVector(b.dir,38*dt);b.life-=dt;
for(const e of enemies)if(e.alive&&b.mesh.position.distanceTo(e.group.position.clone().add(new THREE.Vector3(0,1.4,0)))<1.35){
e.hp--;b.life=0;explosion(b.mesh.position.clone(),0xffc16b);
if(e.hp<=0){e.alive=false;scene.remove(e.group);score++;explosion(e.group.position.clone().add(new THREE.Vector3(0,1,0)));}
break;
}
}
for(const b of [...bullets]){
if(b.life<=0){scene.remove(b.mesh);bullets.splice(bullets.indexOf(b),1);}
}
for(const e of enemies){
if(!e.alive)continue;
e.cooldown-=dt;
if(e.cooldown<=0){enemyShoot(e);e.cooldown=1.3+Math.random()*1.5;}
}
for(const b of [...enemyShots]){
b.mesh.position.addScaledVector(b.dir,15*dt);b.life-=dt;
if(b.mesh.position.distanceTo(drone.position)<1.8){
energy-=8;b.life=0;explosion(drone.position.clone(),0xff4b2e);
}
if(b.life<=0){scene.remove(b.mesh);enemyShots.splice(enemyShots.indexOf(b),1);}
}
for(const p of [...particles]){
p.mesh.position.addScaledVector(p.vel,dt);p.vel.y-=4*dt;p.life-=dt;
p.mesh.scale.multiplyScalar(.985);
if(p.life<=0){scene.remove(p.mesh);particles.splice(particles.indexOf(p),1);}
}
const camOffset=new THREE.Vector3(0,5.5,12).applyAxisAngle(new THREE.Vector3(0,1,0),drone.rotation.y);
const desired=drone.position.clone().add(camOffset);
camera.position.lerp(desired,1-Math.pow(.001,dt));
camera.lookAt(drone.position.x,drone.position.y+.5,drone.position.z-5);
document.getElementById("status").textContent=
`BLANCOS: ${score} · ENERGÍA: ${Math.max(0,Math.floor(energy))}%`;
if(energy<=0){
document.getElementById("status").textContent="DRONE DESTRUIDO · RECARGA LA PÁGINA PARA REINICIAR";
}
renderer.render(scene,camera);
}
animate();
addEventListener("resize",()=>{
camera.aspect=innerWidth/innerHeight;camera.updateProjectionMatrix();
renderer.setSize(innerWidth,innerHeight);
});
</script>
</body>
</html>
```
La cámara sigue al dron desde atrás y por encima; el escenario, los edificios y los robots son modelos 3D sencillos, no una reproducción exacta de la imagen.