|
| 1 | +<!DOCTYPE html> |
| 2 | +<html lang="es"> |
| 3 | +<head> |
| 4 | + <meta charset="UTF-8"> |
| 5 | + <title>Rubber Rocket Arena - Procedural Loop Edition</title> |
| 6 | + <style> |
| 7 | + body { margin: 0; background: #000; overflow: hidden; font-family: 'Segoe UI', Tahoma, sans-serif; color: white; user-select: none;} |
| 8 | + canvas { display: block; background: #0a0a0f; margin: 0 auto; } |
| 9 | + |
| 10 | + .ui { position: absolute; top: 15px; left: 15px; background: rgba(0,0,0,0.85); padding: 15px; border-radius: 12px; border: 1px solid #444; width: 220px; pointer-events: none; } |
| 11 | + .stat-row { display: flex; align-items: center; justify-content: space-between; margin-bottom: 6px; font-size: 13px; font-weight: bold; } |
| 12 | + .bar-container { width: 130px; height: 12px; background: #222; border-radius: 6px; overflow: hidden; border: 1px solid #555; } |
| 13 | + .bar-fill { height: 100%; transition: width 0.15s ease-out; } |
| 14 | + .level-card { text-align: center; margin-bottom: 12px; border-bottom: 1px solid #333; padding-bottom: 10px; } |
| 15 | + .kdr-box { font-size: 16px; font-weight: bold; color: #ffcc00; text-align: center; margin-top: 5px; } |
| 16 | + |
| 17 | + .sandbox-panel { |
| 18 | + position: absolute; top: 15px; right: 15px; |
| 19 | + background: rgba(20, 20, 25, 0.95); padding: 15px; |
| 20 | + border-radius: 12px; border: 1px solid #00ffaa; |
| 21 | + width: 260px; pointer-events: all; font-size: 12px; |
| 22 | + } |
| 23 | + .sandbox-panel h3 { margin: 0 0 10px 0; color: #00ffaa; text-align: center; font-size: 14px; text-transform: uppercase; } |
| 24 | + .control-group { margin-bottom: 10px; } |
| 25 | + .control-group label { display: block; margin-bottom: 3px; color: #ccc; } |
| 26 | + .control-group input { width: 100%; cursor: pointer; accent-color: #00ffaa; } |
| 27 | + .val-display { float: right; color: #00ffaa; font-family: monospace; } |
| 28 | + </style> |
| 29 | +</head> |
| 30 | +<body> |
| 31 | + <div class="ui"> |
| 32 | + <div class="level-card"> |
| 33 | + <h2 id="level-title" style="margin:0; color:#00ffaa; font-size: 18px;">NIVEL 1</h2> |
| 34 | + <div style="font-size:10px; color:#888;">OBJETIVO: <span id="goal-text">10</span> BAJAS</div> |
| 35 | + </div> |
| 36 | + <div class="stat-row"><span>HP</span><div class="bar-container"><div id="hp-bar" class="bar-fill" style="background: linear-gradient(90deg, #ff416c, #ff4b2b); width:100%;"></div></div></div> |
| 37 | + <div class="stat-row"><span>JET</span><div class="bar-container"><div id="jet-bar" class="bar-fill" style="background: linear-gradient(90deg, #00d2ff, #3a7bd5); width:100%;"></div></div></div> |
| 38 | + <div class="kdr-box">K: <span id="k-val">0</span> | D: <span id="d-val">0</span> | KDR: <span id="kdr-val">0.00</span><br><h5>CONTROLES: WASD + ESPACIO<h5></div> |
| 39 | + </div> |
| 40 | + |
| 41 | + <div class="sandbox-panel"> |
| 42 | + <h3>Physics Sandbox</h3> |
| 43 | + <div class="control-group"><label>Gravedad <span class="val-display" id="v-grav">0.60</span></label><input type="range" min="0" max="2" step="0.05" value="0.6" oninput="updatePhys('gravity', this.value, 'v-grav')"></div> |
| 44 | + <div class="control-group"><label>Elasticidad <span class="val-display" id="v-elast">0.80</span></label><input type="range" min="0" max="1.5" step="0.05" value="0.8" oninput="updatePhys('elasticity', this.value, 'v-elast')"></div> |
| 45 | + <div class="control-group"><label>Fricción Aire <span class="val-display" id="v-fric">0.985</span></label><input type="range" min="0.9" max="1" step="0.005" value="0.985" oninput="updatePhys('friction', this.value, 'v-fric')"></div> |
| 46 | + <div class="control-group"><label>Gravedad Proyectil <span class="val-display" id="v-rgrav">0.15</span></label><input type="range" min="-0.1" max="1" step="0.05" value="0.15" oninput="updatePhys('rocketGravity', this.value, 'v-rgrav')"></div> |
| 47 | + <div class="control-group"><label>Velocidad Proyectil <span class="val-display" id="v-rspd">25</span></label><input type="range" min="5" max="60" step="1" value="25" oninput="updatePhys('rocketSpeed', this.value, 'v-rspd')"></div> |
| 48 | + </div> |
| 49 | + |
| 50 | + <canvas id="gameCanvas"></canvas> |
| 51 | + |
| 52 | +<script> |
| 53 | +const canvas = document.getElementById('gameCanvas'); |
| 54 | +const ctx = canvas.getContext('2d'); |
| 55 | +canvas.width = window.innerWidth; |
| 56 | +canvas.height = window.innerHeight; |
| 57 | + |
| 58 | +// --- GENERADOR PSEUDO-ALEATORIO (Seed) --- |
| 59 | +let seed = 12345; |
| 60 | +function mulberry32() { |
| 61 | + return function() { |
| 62 | + let t = seed += 0x6D2B79F5; |
| 63 | + t = Math.imul(t ^ t >>> 15, t | 1); |
| 64 | + t ^= t + Math.imul(t ^ t >>> 7, t | 61); |
| 65 | + return ((t ^ t >>> 14) >>> 0) / 4294967296; |
| 66 | + } |
| 67 | +} |
| 68 | +let random = mulberry32(); |
| 69 | + |
| 70 | +const PHYS = { |
| 71 | + gravity: 0.6, friction: 0.985, elasticity: 0.8, |
| 72 | + squishRecover: 0.12, baseRadius: 25, |
| 73 | + rocketGravity: 0.15, rocketSpeed: 25 |
| 74 | +}; |
| 75 | + |
| 76 | +function updatePhys(param, val, displayId) { |
| 77 | + PHYS[param] = parseFloat(val); |
| 78 | + document.getElementById(displayId).innerText = PHYS[param].toFixed(param === 'rocketSpeed' ? 0 : 2); |
| 79 | +} |
| 80 | + |
| 81 | +let gameState = { |
| 82 | + level: 1, kills: 0, deaths: 0, |
| 83 | + targetKills: 10, enemiesCount: 2, isTransitioning: false, |
| 84 | + shake: 0 |
| 85 | +}; |
| 86 | + |
| 87 | +const keys = {}; |
| 88 | +let entities = []; |
| 89 | +let rockets = []; |
| 90 | +let particles = []; |
| 91 | +let map = []; |
| 92 | + |
| 93 | +// --- CLASE PLATAFORMA MÓVIL --- |
| 94 | +class Platform { |
| 95 | + constructor(x1, y1, x2, y2, w, h, speed) { |
| 96 | + this.x1 = x1; this.y1 = y1; |
| 97 | + this.x2 = x2; this.y2 = y2; |
| 98 | + this.w = w; this.h = h; |
| 99 | + this.speed = speed; |
| 100 | + this.progress = 0; |
| 101 | + this.dir = 1; |
| 102 | + this.x = x1; this.y = y1; |
| 103 | + } |
| 104 | + update() { |
| 105 | + this.progress += this.speed * this.dir; |
| 106 | + if(this.progress >= 1 || this.progress <= 0) this.dir *= -1; |
| 107 | + // Interpolación lineal (LERP) para el movimiento suave |
| 108 | + this.x = this.x1 + (this.x2 - this.x1) * this.progress; |
| 109 | + this.y = this.y1 + (this.y2 - this.y1) * this.progress; |
| 110 | + } |
| 111 | + draw() { |
| 112 | + ctx.fillStyle = '#1e1e2f'; |
| 113 | + ctx.fillRect(this.x, this.y, this.w, this.h); |
| 114 | + ctx.strokeStyle = "#3a3a5a"; |
| 115 | + ctx.lineWidth = 2; |
| 116 | + ctx.strokeRect(this.x, this.y, this.w, this.h); |
| 117 | + } |
| 118 | +} |
| 119 | + |
| 120 | +function generateProceduralLevel(levelNum) { |
| 121 | + seed = 12345 + (levelNum * 777); // Cambia la semilla por nivel |
| 122 | + random = mulberry32(); |
| 123 | + map = []; |
| 124 | + |
| 125 | + // Suelo estático |
| 126 | + map.push(new Platform(0, canvas.height - 40, 0, canvas.height - 40, canvas.width, 40, 0)); |
| 127 | + |
| 128 | + // Generar plataformas según el nivel |
| 129 | + const platformCount = 3 + levelNum; |
| 130 | + for(let i=0; i<platformCount; i++) { |
| 131 | + let w = 50 + random() * 100; |
| 132 | + let h = 20; |
| 133 | + let x1 = random() * (canvas.width - w); |
| 134 | + let y1 = 150 + random() * (canvas.height - 300); |
| 135 | + |
| 136 | + // El loop final (x2, y2) |
| 137 | + let x2 = x1 + (random() - 0.5) * 300; |
| 138 | + let y2 = y1 + (random() - 0.5) * 200; |
| 139 | + |
| 140 | + // Limitar bordes |
| 141 | + x2 = Math.max(0, Math.min(canvas.width - w, x2)); |
| 142 | + y2 = Math.max(100, Math.min(canvas.height - 100, y2)); |
| 143 | + |
| 144 | + let speed = 0.002 + random() * 0.008 + (levelNum * 0.0005); |
| 145 | + if (levelNum <= 2){ |
| 146 | + speed = 0; |
| 147 | + } |
| 148 | + map.push(new Platform(x1, y1, x2, y2, w, h, speed)); |
| 149 | + } |
| 150 | +} |
| 151 | + |
| 152 | +// (Las clases Particle, Rocket y Soldier se mantienen con ajustes para detectar 'map[i].x/y' dinámico) |
| 153 | + |
| 154 | +class Particle { |
| 155 | + constructor(x, y, vx, vy, color, life, size, bouncy = false) { |
| 156 | + this.x = x; this.y = y; this.vx = vx; this.vy = vy; |
| 157 | + this.color = color; this.life = life; this.maxLife = life; |
| 158 | + this.size = size; this.bouncy = bouncy; |
| 159 | + } |
| 160 | + update() { |
| 161 | + if(this.bouncy) this.vy += PHYS.gravity; |
| 162 | + this.x += this.vx; this.y += this.vy; |
| 163 | + if(this.bouncy) { |
| 164 | + if(this.y > canvas.height-40) { this.vy *= -PHYS.elasticity * 0.7; this.y = canvas.height-41; } |
| 165 | + } |
| 166 | + this.life--; |
| 167 | + } |
| 168 | + draw() { |
| 169 | + ctx.globalAlpha = Math.max(0, this.life / this.maxLife); |
| 170 | + ctx.fillStyle = this.color; |
| 171 | + ctx.fillRect(this.x, this.y, this.size, this.size); |
| 172 | + ctx.globalAlpha = 1; |
| 173 | + } |
| 174 | +} |
| 175 | + |
| 176 | +class Rocket { |
| 177 | + constructor(x, y, tx, ty, owner) { |
| 178 | + this.x = x; this.y = y; this.owner = owner; |
| 179 | + const angle = Math.atan2(ty - y, tx - x); |
| 180 | + this.vx = Math.cos(angle) * PHYS.rocketSpeed; |
| 181 | + this.vy = Math.sin(angle) * PHYS.rocketSpeed; |
| 182 | + this.dead = false; |
| 183 | + } |
| 184 | + update() { |
| 185 | + this.vy += PHYS.rocketGravity; |
| 186 | + this.x += this.vx; this.y += this.vy; |
| 187 | + if(this.y < 0 || this.y > canvas.height - 40 || this.x < 0 || this.x > canvas.width) { |
| 188 | + this.explode(); return; |
| 189 | + } |
| 190 | + for(let p of map) { |
| 191 | + if(this.x > p.x && this.x < p.x + p.w && this.y > p.y && this.y < p.y + p.h) { |
| 192 | + this.explode(); return; |
| 193 | + } |
| 194 | + } |
| 195 | + for(let e of entities) { |
| 196 | + if(e !== this.owner && Math.hypot(e.x - this.x, e.y - this.y) < e.radius) { |
| 197 | + this.explode(); return; |
| 198 | + } |
| 199 | + } |
| 200 | + } |
| 201 | + explode() { |
| 202 | + this.dead = true; |
| 203 | + for(let i=0; i<15; i++) particles.push(new Particle(this.x, this.y, (Math.random()-0.5)*12, (Math.random()-0.5)*12, '#ff9900', 20, 4)); |
| 204 | + entities.forEach(e => { |
| 205 | + let dist = Math.hypot(e.x - this.x, e.y - this.y); |
| 206 | + if(dist < 120) { |
| 207 | + let pct = (1 - dist/120); |
| 208 | + let ang = Math.atan2(e.y - this.y, e.x - this.x); |
| 209 | + e.vx += Math.cos(ang) * pct * 32 * (PHYS.baseRadius / e.radius); |
| 210 | + e.vy += Math.sin(ang) * pct * 32 * (PHYS.baseRadius / e.radius); |
| 211 | + e.hp -= pct * 115; |
| 212 | + if(e.hp <= 0 && this.owner.isPlayer) updateStats(true); |
| 213 | + } |
| 214 | + }); |
| 215 | + } |
| 216 | +} |
| 217 | + |
| 218 | +class Soldier { |
| 219 | + constructor(x, y, color, isPlayer = false) { |
| 220 | + this.x = x; this.y = y; this.color = color; |
| 221 | + this.vx = 0; this.vy = 0; this.hp = 100; this.jet = 100; |
| 222 | + this.isPlayer = isPlayer; this.lastShot = 0; |
| 223 | + this.radius = PHYS.baseRadius; |
| 224 | + this.sqX = 1; this.sqY = 1; |
| 225 | + } |
| 226 | + update() { |
| 227 | + this.radius = Math.max(6, (this.hp / 100) * PHYS.baseRadius); |
| 228 | + this.vy += PHYS.gravity; |
| 229 | + if(this.isPlayer) { |
| 230 | + if(keys['KeyA']) this.vx -= 1.3; if(keys['KeyD']) this.vx += 1.3; |
| 231 | + if(keys['KeyW'] && this.jet > 0) { this.vy -= 1.8; this.jet -= 2.8; } |
| 232 | + else { this.jet = Math.min(100, this.jet + 1.4); } |
| 233 | + if(keys['Space']) this.shoot(); |
| 234 | + } else { |
| 235 | + if(Math.random() < 0.02) this.shoot(); |
| 236 | + this.vx += (player.x > this.x ? 0.35 : -0.35); |
| 237 | + } |
| 238 | + this.vx *= PHYS.friction; |
| 239 | + this.x += this.vx; this.y += this.vy; |
| 240 | + |
| 241 | + if(this.y + this.radius > canvas.height - 40) { |
| 242 | + this.y = canvas.height - 40 - this.radius; this.vy *= -PHYS.elasticity; this.sqY = 0.5; this.sqX = 1.4; |
| 243 | + } else if(this.y - this.radius < 0) { |
| 244 | + this.y = this.radius; this.vy *= -PHYS.elasticity; this.sqY = 0.5; this.sqX = 1.4; |
| 245 | + } |
| 246 | + if(this.x - this.radius < 0) { this.x = this.radius; this.vx *= -PHYS.elasticity; } |
| 247 | + if(this.x + this.radius > canvas.width) { this.x = canvas.width - this.radius; this.vx *= -PHYS.elasticity; } |
| 248 | + |
| 249 | + map.forEach(p => { |
| 250 | + if(this.x + this.radius > p.x && this.x - this.radius < p.x + p.w && this.y + this.radius > p.y && this.y - this.radius < p.y + p.h) { |
| 251 | + if(this.y < p.y + p.h/2) { |
| 252 | + this.y = p.y - this.radius; |
| 253 | + this.vy *= -PHYS.elasticity; |
| 254 | + // Si la plataforma se mueve, arrastramos levemente al personaje |
| 255 | + this.x += (p.x2 - p.x1) * p.speed * p.dir; |
| 256 | + } else { |
| 257 | + this.y = p.y + p.h + this.radius; this.vy *= -PHYS.elasticity; |
| 258 | + } |
| 259 | + } |
| 260 | + }); |
| 261 | + |
| 262 | + this.sqX += (1 - this.sqX) * PHYS.squishRecover; |
| 263 | + this.sqY += (1 - this.sqY) * PHYS.squishRecover; |
| 264 | + |
| 265 | + if(this.hp <= 0) this.die(); |
| 266 | + } |
| 267 | + shoot() { |
| 268 | + if(Date.now() - this.lastShot < 650) return; |
| 269 | + let tx = this.isPlayer ? (entities.find(e=>!e.isPlayer)?.x || this.x+100) : player.x; |
| 270 | + let ty = this.isPlayer ? (entities.find(e=>!e.isPlayer)?.y || this.y) : player.y; |
| 271 | + rockets.push(new Rocket(this.x, this.y, tx, ty, this)); |
| 272 | + this.lastShot = Date.now(); |
| 273 | + } |
| 274 | + die() { |
| 275 | + for(let i=0; i<30; i++) particles.push(new Particle(this.x, this.y, (Math.random()-0.5)*15, (Math.random()-0.5)*15, '#ff0000', 15, Math.random()*10)); |
| 276 | + if(this.isPlayer) { |
| 277 | + gameState.shake = 35; updateStats(false); |
| 278 | + this.hp = 100; this.x = canvas.width/2; this.y = 100; this.vx = 0; this.vy = 0; |
| 279 | + } else { |
| 280 | + this.hp = 100; this.x = Math.random()*canvas.width; this.y = -100; |
| 281 | + } |
| 282 | + } |
| 283 | + draw() { |
| 284 | + ctx.save(); |
| 285 | + ctx.translate(this.x, this.y); |
| 286 | + ctx.scale(this.sqX, this.sqY); |
| 287 | + ctx.fillStyle = this.color; |
| 288 | + ctx.beginPath(); ctx.arc(0, 0, this.radius, 0, Math.PI*2); ctx.fill(); |
| 289 | + ctx.strokeStyle = "white"; ctx.stroke(); |
| 290 | + ctx.restore(); |
| 291 | + } |
| 292 | +} |
| 293 | + |
| 294 | +function updateStats(isKill) { |
| 295 | + if(isKill) gameState.kills++; else gameState.deaths++; |
| 296 | + document.getElementById('k-val').innerText = gameState.kills; |
| 297 | + document.getElementById('d-val').innerText = gameState.deaths; |
| 298 | + let kdr = gameState.deaths === 0 ? gameState.kills : (gameState.kills / gameState.deaths); |
| 299 | + document.getElementById('kdr-val').innerText = kdr.toFixed(2); |
| 300 | + if(gameState.kills >= gameState.targetKills && !gameState.isTransitioning) nextLevel(); |
| 301 | +} |
| 302 | + |
| 303 | +function nextLevel() { |
| 304 | + gameState.isTransitioning = true; |
| 305 | + gameState.level++; |
| 306 | + gameState.targetKills += 5; |
| 307 | + gameState.enemiesCount++; |
| 308 | + document.getElementById('level-title').innerText = "Nivel " + gameState.level; |
| 309 | + document.getElementById('goal-text').innerText = gameState.targetKills; |
| 310 | + initLevel(); |
| 311 | + setTimeout(() => { gameState.isTransitioning = false; }, 1000); |
| 312 | +} |
| 313 | + |
| 314 | +const player = new Soldier(canvas.width/2, 100, '#00ff00', true); |
| 315 | + |
| 316 | +function initLevel() { |
| 317 | + generateProceduralLevel(gameState.level); |
| 318 | + entities = [player]; |
| 319 | + for(let i=0; i < gameState.enemiesCount; i++) { |
| 320 | + entities.push(new Soldier(random()*canvas.width, -100, '#ff4444')); |
| 321 | + } |
| 322 | +} |
| 323 | + |
| 324 | +window.addEventListener('keydown', e => keys[e.code] = true); |
| 325 | +window.addEventListener('keyup', e => keys[e.code] = false); |
| 326 | + |
| 327 | +function loop() { |
| 328 | + ctx.save(); |
| 329 | + if (gameState.shake > 0) { |
| 330 | + ctx.translate((Math.random()-0.5)*gameState.shake, (Math.random()-0.5)*gameState.shake); |
| 331 | + gameState.shake *= 0.85; |
| 332 | + } |
| 333 | + ctx.clearRect(-100, -100, canvas.width+200, canvas.height+200); |
| 334 | + |
| 335 | + map.forEach(p => { p.update(); p.draw(); }); |
| 336 | + rockets = rockets.filter(r => !r.dead); |
| 337 | + rockets.forEach(r => { r.update(); ctx.fillStyle="white"; ctx.fillRect(r.x-3, r.y-3, 6, 6); }); |
| 338 | + particles = particles.filter(p => p.life > 0); |
| 339 | + particles.forEach(p => { p.update(); p.draw(); }); |
| 340 | + entities.forEach(e => { e.update(); e.draw(); }); |
| 341 | + |
| 342 | + ctx.restore(); |
| 343 | + document.getElementById('hp-bar').style.width = player.hp + '%'; |
| 344 | + document.getElementById('jet-bar').style.width = player.jet + '%'; |
| 345 | + requestAnimationFrame(loop); |
| 346 | +} |
| 347 | + |
| 348 | +initLevel(); |
| 349 | +loop(); |
| 350 | +</script> |
| 351 | +</body> |
| 352 | +</html> |
0 commit comments