RIFT ISLAND - Battle Royale
by CyberMeteor92324 lines13.5 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">
<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.