🎮ArcadeLab

RIFT ISLAND - Battle Royale

by CyberMeteor92
324 lines13.5 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">
<title>RIFT ISLAND - Battle Royale</title>
<style>
*{box-sizing:border-box}
body{margin:0;overflow:hidden;background:#78c9ef;font-family:Arial;color:white}
canvas{display:block}
#ui{position:fixed;inset:0;pointer-events:none;text-shadow:1px 2px 3px #111}
.box{background:#101c2bd9;border:1px solid #ffffff35;border-radius:10px;padding:10px}
#stats{position:absolute;top:12px;left:12px;width:185px}
#info{position:absolute;top:12px;right:12px;text-align:right}
#cross{position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);font-size:25px}
#bottom{position:absolute;bottom:15px;left:50%;transform:translateX(-50%);text-align:center;white-space:nowrap}
#message{position:absolute;top:30%;left:50%;transform:translateX(-50%);font-size:25px;font-weight:bold;text-align:center}
.bar{height:10px;background:#343434;border-radius:8px;overflow:hidden;margin:5px 0}
.fill{height:100%;width:100%;background:#35e57c}
#shield{background:#32aaff}
#start{position:fixed;inset:0;z-index:5;background:#10233deF;display:flex;align-items:center;justify-content:center;text-align:center}
.card{padding:25px;max-width:500px}
h1{font-size:40px;color:#6fffd2;margin:10px}
button{pointer-events:auto;background:#ffcf35;border:0;border-radius:8px;padding:14px 25px;font-weight:bold;font-size:17px;cursor:pointer}
.small{line-height:1.7;color:#d4e2ef;font-size:14px}
#map{position:absolute;right:12px;bottom:12px;width:120px;height:120px;border:2px solid white;border-radius:50%;background:#427c42}
@media(max-width:600px){#stats{font-size:12px;width:155px}#info{font-size:12px}#map{width:85px;height:85px}}
</style>
</head>
<body>
<div id="start"><div class="card">
<h1>⚡ RIFT ISLAND</h1>
<h2>ULTIMATE BATTLE ROYALE</h2>
<p class="small">Sobrevive en una isla llena de enemigos. Encuentra armas, recoge recursos y escapa de la tormenta.</p>
<p class="small">WASD moverse · Ratón apuntar · Clic disparar<br>SHIFT correr · E recoger · Q construir · R recargar</p>
<button id="play">JUGAR AHORA</button>
</div></div>
<div id="ui">
<div id="stats" class="box">
❤️ Vida: <span id="hp">100</span><div class="bar"><div id="health" class="fill"></div></div>
🛡️ Escudo: <span id="sh">50</span><div class="bar"><div id="shield" class="fill"></div></div>
🔫 <span id="weapon">Rifle</span><br>Munición: <span id="ammo">30 / 120</span><br>🧱 Materiales: <span id="mats">100</span>
</div>
<div id="info" class="box">
👾 Enemigos: <span id="bots">15</span><br>
🌪️ Zona: <span id="zone">200</span> m<br>
🏆 Eliminaciones: <span id="kills">0</span>
</div>
<div id="cross">+</div><div id="message"></div>
<div id="bottom" class="box">RIFT ISLAND · ¡Sé el último en pie!</div>
<canvas id="map"></canvas>
</div>
<script src="https://cdn.jsdelivr.net/npm/three@0.160.0/build/three.min.js"></script>
<script>
(()=>{
if(!window.THREE){document.querySelector('#message').textContent='No se pudo cargar el motor 3D';return}
const $=id=>document.getElementById(id);
const scene=new THREE.Scene();
scene.background=new THREE.Color(0x85d4f5);
scene.fog=new THREE.Fog(0x85d4f5,100,320);
const camera=new THREE.PerspectiveCamera(70,innerWidth/innerHeight,.1,800);
const renderer=new THREE.WebGLRenderer({antialias:true});
renderer.setSize(innerWidth,innerHeight);
renderer.setPixelRatio(Math.min(devicePixelRatio,1.5));
renderer.shadowMap.enabled=true;
document.body.appendChild(renderer.domElement);
scene.add(new THREE.HemisphereLight(0xc9eeff,0x53633d,2));
const sun=new THREE.DirectionalLight(0xffedc1,2.5);
sun.position.set(70,100,50);sun.castShadow=true;scene.add(sun);
const material=c=>new THREE.MeshStandardMaterial({color:c,roughness:.85});
const grass=material(0x62a64d),wood=material(0x80512d);
const leaves=material(0x247843),rock=material(0x777f84);
const obstacles=[];
function block(x,y,z,w,h,d,m,solid=true){
 const o=new THREE.Mesh(new THREE.BoxGeometry(w,h,d),m);
 o.position.set(x,y+h/2,z);o.castShadow=true;o.receiveShadow=true;scene.add(o);
 if(solid)obstacles.push({x,z,rx:w/2,rz:d/2});
 return o;
}
const floor=new THREE.Mesh(new THREE.PlaneGeometry(700,700),grass);
floor.rotation.x=-Math.PI/2;floor.receiveShadow=true;scene.add(floor);
const water=new THREE.Mesh(new THREE.PlaneGeometry(1500,1500),material(0x268bb9));
water.rotation.x=-Math.PI/2;water.position.y=-1;scene.add(water);
// Isla, bosque y edificios
for(let i=0;i<145;i++){
 let x=(Math.random()-.5)*570,z=(Math.random()-.5)*570;
 if(Math.hypot(x,z)<18)continue;
 if(Math.random()<.72){
  const h=2+Math.random()*2;
  block(x,h/2,z,.7,h,.7,wood);
  const crown=new THREE.Mesh(new THREE.ConeGeometry(1.5+Math.random(),3.2,7),leaves);
  crown.position.set(x,h+1,z);crown.castShadow=true;scene.add(crown);
 }else{
  const h=4+Math.random()*9,w=5+Math.random()*6;
  block(x,h/2,z,w,h,w,material([0xc8bda5,0xb9cbd1,0xc4a17b][i%3]));
  block(x,h+.12,z,w+.2,.25,w+.2,material(0xa54f46),false);
 }
}
for(let i=0;i<20;i++){
 let x=(Math.random()-.5)*350,z=(Math.random()-.5)*350;
 block(x,.7,z,2,1.4,2,rock);
}
// Tormenta
let radius=200;
const ring=new THREE.Mesh(new THREE.RingGeometry(.99,1,120),
 new THREE.MeshBasicMaterial({color:0x4abfff,side:THREE.DoubleSide}));
ring.rotation.x=-Math.PI/2;ring.position.y=.2;scene.add(ring);
function ringUpdate(){ring.scale.set(radius,radius,1)}
ringUpdate();
// Jugador
const player={p:new THREE.Vector3(),hp:100,shield:50,ammo:30,reserve:120,
 mats:100,kills:0,alive:true,weapon:'Rifle'};
const hero=new THREE.Group();scene.add(hero);
const body=new THREE.Mesh(new THREE.CapsuleGeometry(.5,.9,4,8),material(0x17cbb5));
body.position.y=1.2;hero.add(body);
const head=new THREE.Mesh(new THREE.SphereGeometry(.31,12,10),material(0xf4c39c));
head.position.y=2;hero.add(head);
const gun=block(0,1.4,0,.2,.2,.9,material(0x333d48),false);hero.add(gun);
let yaw=0,pitch=.12,started=false,locked=false,over=false;
const keys={};const clock=new THREE.Clock();
document.addEventListener('keydown',e=>{
 keys[e.code]=true;
 if(e.code==='KeyE')pickup();
 if(e.code==='KeyQ')build();
 if(e.code==='KeyR')reload();
});
document.addEventListener('keyup',e=>keys[e.code]=false);
document.addEventListener('mousemove',e=>{
 if(!locked)return;
 yaw-=e.movementX*.0024;
 pitch=THREE.MathUtils.clamp(pitch-e.movementY*.002,-.45,.65);
});
document.addEventListener('pointerlockchange',()=>locked=document.pointerLockElement===renderer.domElement);
renderer.domElement.addEventListener('click',()=>{
 if(!started||over)return;
 if(!locked)renderer.domElement.requestPointerLock?.();
 else shoot();
});
// Bots
const enemies=[];
function makeBot(x,z){
 const g=new THREE.Group();
 const c=material([0xe9544d,0xb56aff,0xe8a331,0x478cf1][Math.floor(Math.random()*4)]);
 const b=new THREE.Mesh(new THREE.CapsuleGeometry(.5,.85,4,8),c);
 b.position.y=1.1;g.add(b);
 const h=new THREE.Mesh(new THREE.SphereGeometry(.3,10,8),material(0xe4b18c));
 h.position.y=1.9;g.add(h);g.position.set(x,0,z);scene.add(g);
 enemies.push({g,hp:100,cd:1+Math.random()*2,alive:true,speed:2+Math.random()*1.5});
}
for(let i=0;i<15;i++){
 const a=Math.random()*Math.PI*2,r=35+Math.random()*145;
 makeBot(Math.cos(a)*r,Math.sin(a)*r);
}
// Objetos de botín
const loot=[];
function drop(x,z,type){
 const color=type==='ammo'?0xffd34d:type==='shield'?0x3caaff:0x4aff9b;
 const m=new THREE.Mesh(new THREE.BoxGeometry(.7,.7,.7),material(color));
 m.position.set(x,.7,z);scene.add(m);loot.push({m,type,spin:0});
}
for(let i=0;i<40;i++){
 let a=Math.random()*Math.PI*2,r=8+Math.random()*170;
 drop(Math.cos(a)*r,Math.sin(a)*r,['ammo','shield','mats'][i%3]);
}
function updateHUD(){
 $('hp').textContent=Math.ceil(player.hp);
 $('health').style.width=player.hp+'%';
 $('sh').textContent=Math.ceil(player.shield);
 $('shield').style.width=player.shield+'%';
 $('ammo').textContent=player.ammo+' / '+player.reserve;
 $('mats').textContent=player.mats;
 $('kills').textContent=player.kills;
 $('bots').textContent=enemies.filter(b=>b.alive).length;
 $('weapon').textContent=player.weapon;
}
function notify(s){
 $('message').textContent=s;
 clearTimeout(notify.t);
 notify.t=setTimeout(()=>$('message').textContent='',1200);
}
function damage(n){
 let a=Math.min(n,player.shield);player.shield-=a;player.hp-=n-a;
 if(player.hp<=0){player.hp=0;player.alive=false;finish(false)}
 updateHUD();
}
function finish(win){
 if(over)return;over=true;
 $('start').style.display='flex';
 $('start').querySelector('h1').textContent=win?'¡VICTORIA!':'FIN DE LA PARTIDA';
 $('start').querySelector('h2').textContent='Eliminaciones: '+player.kills;
 $('start').querySelector('.small').textContent='Pulsa el botón para volver a jugar.';
 $('play').textContent='VOLVER A JUGAR';
 notify(win?'¡Eres el campeón!':'Has sido eliminado');
}
function shoot(){
 if(!player.alive||player.ammo<=0){notify('¡Recarga con R!');return}
 player.ammo--;
 const origin=hero.position.clone().add(new THREE.Vector3(0,1.5,0));
 const dir=new THREE.Vector3(-Math.sin(yaw)*Math.cos(pitch),Math.sin(pitch),-Math.cos(yaw)*Math.cos(pitch));
 const ray=new THREE.Raycaster(origin,dir,0,100);
 const hits=ray.intersectObjects(enemies.filter(b=>b.alive).map(b=>b.g),true);
 let end=origin.clone().addScaledVector(dir,80);
 if(hits.length){
  end.copy(hits[0].point);
  let ob=hits[0].object;
  while(ob.parent&& !enemies.some(b=>b.g===ob))ob=ob.parent;
  const b=enemies.find(b=>b.g===ob);
  if(b){b.hp-=35;if(b.hp<=0)kill(b)}
 }
 const tracer=new THREE.Mesh(new THREE.BufferGeometry().setFromPoints([origin,end]),
  new THREE.LineBasicMaterial({color:0xfff2a1}));
 scene.add(tracer);setTimeout(()=>scene.remove(tracer),70);
 updateHUD();
}
function kill(b){
 if(!b.alive)return;b.alive=false;scene.remove(b.g);
 player.kills++;drop(b.g.position.x,b.g.position.z,'ammo');
 notify('¡Enemigo eliminado!');updateHUD();
}
function pickup(){
 let best=null,d=3;
 for(const l of loot){const n=l.m.position.distanceTo(player.p);if(n<d){d=n;best=l}}
 if(!best){notify('No hay botín cerca');return}
 if(best.type==='ammo')player.reserve+=30;
 if(best.type==='shield')player.shield=Math.min(100,player.shield+25);
 if(best.type==='mats')player.mats+=30;
 scene.remove(best.m);loot.splice(loot.indexOf(best),1);
 notify('¡Botín recogido!');updateHUD();
}
function reload(){
 let n=Math.min(30-player.ammo,player.reserve);
 player.ammo+=n;player.reserve-=n;updateHUD();notify('Recargando');
}
const walls=[];
function build(){
 if(player.mats<10){notify('Necesitas materiales');return}
 const x=player.p.x-Math.sin(yaw)*3,z=player.p.z-Math.cos(yaw)*3;
 const w=new THREE.Mesh(new THREE.BoxGeometry(4,3,.25),material(0x64cbe9));
 w.position.set(x,1.5,z);w.rotation.y=yaw;scene.add(w);walls.push(w);
 player.mats-=10;updateHUD();notify('¡Pared construida!');
}
function move(pos,dx,dz){
 const nx=pos.x+dx,nz=pos.z+dz;
 if(Math.abs(nx)<290&&!obstacles.some(o=>Math.abs(nx-o.x)<o.rx+.5&&Math.abs(pos.z-o.z)<o.rz+.5))pos.x=nx;
 if(Math.abs(nz)<290&&!obstacles.some(o=>Math.abs(pos.x-o.x)<o.rx+.5&&Math.abs(nz-o.z)<o.rz+.5))pos.z=nz;
}
let elapsed=0,stormDamage=0;
function updateBots(dt){
 for(const b of enemies){
  if(!b.alive)continue;
  const p=b.g.position,dx=player.p.x-p.x,dz=player.p.z-p.z;
  const d=Math.hypot(dx,dz);b.cd-=dt;
  if(d<48&&player.alive){
   b.g.rotation.y=Math.atan2(-dx,-dz);
   if(d>7)move(p,dx/d*b.speed*dt,dz/d*b.speed*dt);
   if(d<27&&b.cd<=0){
    b.cd=1.4+Math.random();
    if(Math.random()<.65)damage(7);
   }
  }else{
   p.x+=Math.sin(elapsed+b.speed)*dt;
   p.z+=Math.cos(elapsed*.7+b.speed)*dt;
  }
  if(Math.hypot(p.x,p.z)>radius)b.hp-=dt*4;
  if(b.hp<=0)kill(b);
 }
}
function updateCamera(){
 const p=hero.position;
 const dest=new THREE.Vector3(
  p.x+Math.sin(yaw)*7,
  p.y+3.2+pitch*2,
  p.z+Math.cos(yaw)*7);
 camera.position.lerp(dest,.16);
 camera.lookAt(p.x,p.y+1.4,p.z);
}
function minimap(){
 const c=$('map'),ctx=c.getContext('2d');
 const w=c.width=c.clientWidth*devicePixelRatio,h=c.height=c.clientHeight*devicePixelRatio;
 ctx.clearRect(0,0,w,h);ctx.save();ctx.translate(w/2,h/2);ctx.scale(w/500,h/500);
 ctx.fillStyle='#4c8741';ctx.fillRect(-250,-250,500,500);
 ctx.strokeStyle='#45c5ff';ctx.lineWidth=3;ctx.beginPath();ctx.arc(0,0,radius,0,Math.PI*2);ctx.stroke();
 enemies.forEach(b=>{if(b.alive){ctx.fillStyle='red';ctx.fillRect(b.g.position.x-3,b.g.position.z-3,6,6)}});
 ctx.fillStyle='white';ctx.beginPath();ctx.arc(player.p.x,player.p.z,5,0,7);ctx.fill();ctx.restore();
}
function animate(){
 requestAnimationFrame(animate);
 const dt=Math.min(clock.getDelta(),.04);
 if(started&&!over){
  elapsed+=dt;
  let f=(keys.KeyW?1:0)-(keys.KeyS?1:0);
  let s=(keys.KeyD?1:0)-(keys.KeyA?1:0);
  let dx=-Math.sin(yaw)*f+Math.cos(yaw)*s;
  let dz=-Math.cos(yaw)*f-Math.sin(yaw)*s;
  const l=Math.hypot(dx,dz)||1;
  const speed=keys.ShiftLeft?13:8;
  move(player.p,dx/l*speed*dt,dz/l*speed*dt);
  hero.position.copy(player.p);hero.rotation.y=yaw;
  gun.position.set(-.5,1.4,-.5);
  updateBots(dt);
  for(const l of loot){l.spin+=dt;l.m.rotation.y=l.spin;l.m.position.y=.7+Math.sin(l.spin*2)*.15}
  if(elapsed>15&&radius>22){radius=Math.max(22,radius-dt*.9);ringUpdate()}
  if(Math.hypot(player.p.x,player.p.z)>radius){stormDamage+=dt;if(stormDamage>.5){damage(3);stormDamage=0}}
  else stormDamage=0;
  $('zone').textContent=Math.round(radius);
  if(enemies.every(b=>!b.alive))finish(true);
  updateCamera();minimap();
 }
 renderer.render(scene,camera);
}
$('play').onclick=()=>{
 if(started){location.reload();return}
 started=true;$('start').style.display='none';
 renderer.domElement.requestPointerLock?.();
};
addEventListener('resize',()=>{
 camera.aspect=innerWidth/innerHeight;camera.updateProjectionMatrix();
 renderer.setSize(innerWidth,innerHeight);
});
updateHUD();animate();
})();
</script>
</body>
</html>

Game Source: RIFT ISLAND - Battle Royale

Creator: CyberMeteor92

Libraries: three

Complexity: complex (324 lines, 13.5 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: rift-island-battle-royale-cybermeteor92" to link back to the original. Then publish at arcadelab.ai/publish.