PAU
a game exactly like vampire surviver but with robots
BUDDY
Awesome idea, Pau! 🤖 A robot survival game sounds so cool! I'm building your very own "Robot Survivor" game right now — get ready to blast some enemy bots!
PAU
can you make it so that the bot is not trapped in one area and that diffrent charecters unlock
BUDDY
Awesome upgrades, Pau! 🤖 Now the world scrolls infinitely so you're never trapped, AND there are 3 different robot characters to unlock as you level up — each with unique stats and looks. Pick your bot and survive!
CODE
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Robot Survivor</title> <style> * { margin: 0; padding: 0; box-sizing: border-box; } body { background: #0a0a1a; display: flex; flex-direction: column; align-items: center; justify-content: center; height: 100vh; font-family: 'Courier New', monospace; color: #00ffcc; overflow: hidden; } #ui { position: absolute; top: 10px; left: 10px; z-index: 10; text-shadow: 0 0 8px #00ffcc; } #ui div { margin: 3px 0; font-size: 15px; } #timer { position: absolute; top: 10px; left: 50%; transform: translateX(-50%); font-size: 22px; font-weight: bold; color: #ffdd00; text-shadow: 0 0 10px #ffaa00; z-index: 10; } canvas { display: block; } #overlay { position: absolute; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.92); display: flex; flex-direction: column; align-items: center; justify-content: center; z-index: 20; } #overlay h1 { font-size: 48px; color: #00ffcc; text-shadow: 0 0 20px #00ffcc; margin-bottom: 10px; } #overlay p { font-size: 16px; color: #aaffee; margin: 6px 0; } .char-select { display: flex; gap: 20px; margin: 24px 0; flex-wrap: wrap; justify-content: center; } .char-card { background: #111133; border: 2px solid #00ffcc44; border-radius: 14px; padding: 18px 22px; cursor: pointer; text-align: center; transition: all 0.15s; min-width: 160px; } .char-card:hover, .char-card.selected { border-color: #00ffcc; box-shadow: 0 0 18px #00ffcc88; background: #001a2a; } .char-card.locked { border-color: #444; opacity: 0.5; cursor: not-allowed; } .char-card h3 { font-size: 16px; color: #00ffcc; margin-bottom: 6px; } .char-card .stats { font-size: 12px; color: #aaa; line-height: 1.7; } .char-card .lock-msg { font-size: 11px; color: #ff8844; margin-top: 6px; } #startBtn { margin-top: 18px; padding: 14px 40px; font-size: 20px; background: #00ffcc; color: #0a0a1a; border: none; border-radius: 8px; cursor: pointer; font-family: 'Courier New', monospace; font-weight: bold; box-shadow: 0 0 20px #00ffcc; transition: transform 0.1s; } #startBtn:hover { transform: scale(1.07); } #levelup { position: absolute; top: 50%; left: 50%; transform: translate(-50%,-50%); background: rgba(0,0,30,0.97); border: 2px solid #ffdd00; border-radius: 16px; z-index: 30; padding: 30px 20px; text-align: center; display: none; min-width: 340px; } #levelup h2 { color: #ffdd00; font-size: 26px; margin-bottom: 18px; text-shadow: 0 0 10px #ffaa00; } .upgrade-btn { display: block; width: 100%; margin: 8px 0; padding: 12px; background: #111133; border: 2px solid #00ffcc; color: #00ffcc; font-size: 15px; border-radius: 8px; cursor: pointer; font-family: 'Courier New', monospace; transition: background 0.15s; } .upgrade-btn:hover { background: #00ffcc; color: #0a0a1a; } #xpbar-wrap { position: absolute; bottom: 10px; left: 50%; transform: translateX(-50%); width: 400px; background: #111; border: 1px solid #00ffcc; border-radius: 8px; height: 16px; z-index: 10; } #xpbar { height: 100%; background: linear-gradient(90deg, #00ffcc, #00aaff); border-radius: 8px; width: 0%; transition: width 0.2s; } #minimap { position: absolute; top: 10px; right: 10px; z-index: 10; border: 1px solid #00ffcc44; border-radius: 4px; } </style> </head> <body> <div id="ui"> <div>❤️ HP: <span id="hpText">100</span></div> <div>⚡ LVL: <span id="lvlText">1</span></div> <div>☠️ Kills: <span id="killText">0</span></div> <div id="charLabel" style="color:#ffdd00;margin-top:4px;"></div> </div> <div id="timer">00:00</div> <canvas id="c"></canvas> <canvas id="minimap" width="100" height="75"></canvas> <div id="xpbar-wrap"><div id="xpbar"></div></div> <!-- Start / Character Select overlay --> <div id="overlay"> <h1>🤖 ROBOT SURVIVOR</h1> <p>Move: <b>WASD</b> or <b>Arrow Keys</b> | World is <b>infinite</b>!</p> <p style="color:#ffdd00;margin-top:4px;">Choose your Robot:</p> <div class="char-select" id="charSelect"></div> <div id="charDesc" style="color:#aaffee;font-size:14px;min-height:20px;"></div> <button id="startBtn">START GAME</button> <p id="unlockHint" style="margin-top:12px;font-size:13px;color:#888;"></p> </div> <!-- Level up overlay --> <div id="levelup"> <h2>⬆️ LEVEL UP!</h2> <div id="upgradeOptions"></div> </div> <script> // ---- Canvas Setup ---- const canvas = document.getElementById('c'); const ctx = canvas.getContext('2d'); const miniCanvas = document.getElementById('minimap'); const mctx = miniCanvas.getContext('2d'); function resizeCanvas() { canvas.width = window.innerWidth; canvas.height = window.innerHeight; } resizeCanvas(); window.addEventListener('resize', resizeCanvas); const VIEW_W = () => canvas.width; const VIEW_H = () => canvas.height; // ---- Characters ---- const CHARACTERS = [ { id: 'scout', name: '⚡ Scout Bot', desc: 'Fast and agile, but fragile. Starts with Laser.', color: '#00ffcc', bodyColor: '#00ffcc', eyeColor: '#003311', hp: 80, maxHp: 80, speed: 3.5, startWeapon: 'laser', unlockLevel: 0, unlockMsg: '', special: 'speed', }, { id: 'tank', name: '🛡️ Tank Bot', desc: 'Slow but very tough. Starts with Orbit Drones.', color: '#ff6644', bodyColor: '#ff6644', eyeColor: '#330000', hp: 160, maxHp: 160, speed: 1.8, startWeapon: 'orb', unlockLevel: 5, unlockMsg: 'Reach Level 5 total to unlock', special: 'tank', }, { id: 'sniper', name: '🎯 Sniper Bot', desc: 'Balanced stats. Starts with Missile Launcher.', color: '#aa66ff', bodyColor: '#aa66ff', eyeColor: '#110022', hp: 100, maxHp: 100, speed: 2.6, startWeapon: 'missile', unlockLevel: 10, unlockMsg: 'Reach Level 10 total to unlock', special: 'range', }, ]; // Persistent unlocks stored in memory (resets on page reload, but tracks across games this session) let totalLevelsEver = 0; let unlockedChars = new Set(['scout']); function refreshUnlocks() { CHARACTERS.forEach(c => { if (totalLevelsEver >= c.unlockLevel) unlockedChars.add(c.id); }); } let selectedCharId = 'scout'; function buildCharSelect() { refreshUnlocks(); const container = document.getElementById('charSelect'); container.innerHTML = ''; CHARACTERS.forEach(c => { const locked = !unlockedChars.has(c.id); const card = document.createElement('div'); card.className = 'char-card' + (locked ? ' locked' : '') + (selectedCharId === c.id ? ' selected' : ''); // Draw mini robot preview const previewCanvas = document.createElement('canvas'); previewCanvas.width = 60; previewCanvas.height = 70; drawRobotPreview(previewCanvas.getContext('2d'), c, 30, 38); card.appendChild(previewCanvas); const h3 = document.createElement('h3'); h3.style.color = c.color; h3.textContent = c.name; card.appendChild(h3); const stats = document.createElement('div'); stats.className = 'stats'; stats.innerHTML = `❤️ HP: ${c.hp}<br>⚡ SPD: ${c.speed.toFixed(1)}<br>🔫 ${c.startWeapon.charAt(0).toUpperCase()+c.startWeapon.slice(1)}`; card.appendChild(stats); if (locked) { const lm = document.createElement('div'); lm.className = 'lock-msg'; lm.textContent = '🔒 ' + c.unlockMsg; card.appendChild(lm); } if (!locked) { card.onclick = () => { selectedCharId = c.id; buildCharSelect(); document.getElementById('charDesc').textContent = c.desc; }; } container.appendChild(card); }); const hint = CHARACTERS.filter(c => !unlockedChars.has(c.id)); document.getElementById('unlockHint').textContent = hint.length > 0 ? `🔒 Keep playing to unlock more robots! (Session total levels: ${totalLevelsEver})` : '🎉 All robots unlocked!'; // set desc const sel = CHARACTERS.find(c => c.id === selectedCharId); document.getElementById('charDesc').textContent = sel ? sel.desc : ''; } function drawRobotPreview(c2d, charDef, cx, cy) { c2d.save(); c2d.translate(cx, cy); c2d.shadowColor = charDef.color; c2d.shadowBlur = 10; if (charDef.id === 'scout') { c2d.fillStyle = charDef.bodyColor; c2d.fillRect(-10,-8,20,18); c2d.fillStyle = '#aaffee'; c2d.fillRect(-7,-16,14,10); c2d.fillStyle = charDef.eyeColor; c2d.fillRect(-5,-13,4,4); c2d.fillRect(1,-13,4,4); c2d.fillStyle = '#00ffaa'; c2d.fillRect(-4,-12,2,2); c2d.fillRect(2,-12,2,2); c2d.fillStyle = charDef.bodyColor; c2d.fillRect(-9,10,6,7); c2d.fillRect(3,10,6,7); } else if (charDef.id === 'tank') { c2d.fillStyle = charDef.bodyColor; c2d.fillRect(-14,-10,28,22); c2d.fillStyle = '#ffccaa'; c2d.fillRect(-10,-18,20,12); c2d.fillStyle = charDef.eyeColor; c2d.fillRect(-7,-15,5,5); c2d.fillRect(2,-15,5,5); c2d.fillStyle = '#ff9944'; c2d.fillRect(-6,-14,2,3); c2d.fillRect(3,-14,2,3); c2d.fillStyle = charDef.bodyColor; c2d.fillRect(-12,12,9,9); c2d.fillRect(3,12,9,9); // armor plates c2d.fillStyle = '#cc4422'; c2d.fillRect(-14,-10,5,22); c2d.fillRect(9,-10,5,22); } else if (charDef.id === 'sniper') { c2d.fillStyle = charDef.bodyColor; c2d.fillRect(-9,-7,18,20); c2d.fillStyle = '#ddaaff'; c2d.fillRect(-7,-16,14,11); c2d.fillStyle = charDef.eyeColor; c2d.fillRect(-5,-13,3,4); c2d.fillRect(2,-13,3,4); c2d.fillStyle = '#cc88ff'; c2d.fillRect(-4,-12,2,2); c2d.fillRect(3,-12,2,2); c2d.fillStyle = charDef.bodyColor; c2d.fillRect(-8,13,6,8); c2d.fillRect(2,13,6,8); // sniper barrel c2d.fillStyle = '#9955ee'; c2d.fillRect(7,-3,14,4); } c2d.shadowBlur = 0; c2d.restore(); } // ---- Game State ---- let state = 'menu'; let camX, camY; // camera offset (world coords of top-left) let player, enemies, bullets, particles, xpOrbs; let kills, elapsed, lastTime, keys, spawnTimer, spawnInterval; let lvl, xp, xpNeeded, levelingUp; let weaponLevels, passives; let currentChar; // World is infinite — camera follows player // player.wx, player.wy = world position function initGame() { currentChar = CHARACTERS.find(c => c.id === selectedCharId); player = { wx: 0, wy: 0, // world position r: currentChar.id === 'tank' ? 18 : 16, hp: currentChar.hp, maxHp: currentChar.maxHp, speed: currentChar.speed, invincible: 0, facing: 0, }; camX = player.wx - VIEW_W()/2; camY = player.wy - VIEW_H()/2; enemies = []; bullets = []; particles = []; xpOrbs = []; kills = 0; elapsed = 0; lastTime = null; keys = {}; spawnTimer = 0; spawnInterval = 2.0; lvl = 1; xp = 0; xpNeeded = 20; levelingUp = false; weaponLevels = { laser: 0, orb: 0, missile: 0 }; passives = { speed: 0, hp: 0, xpRange: 0 }; weaponLevels[currentChar.startWeapon] = 1; weaponTimers = { laser: 0, orb: 0, missile: 0, orbAngle: 0 }; document.getElementById('levelup').style.display = 'none'; document.getElementById('charLabel').textContent = currentChar.name; updateUI(); } // ---- Input ---- document.addEventListener('keydown', e => { keys[e.key] = true; e.preventDefault && ['ArrowUp','ArrowDown','ArrowLeft','ArrowRight',' '].includes(e.key) && e.preventDefault(); }); document.addEventListener('keyup', e => { keys[e.key] = false; }); // ---- Upgrade System ---- const ALL_UPGRADES = [ { id: 'laser', label: '🔫 Laser Cannon', desc: 'Shoots faster & harder lasers', type: 'weapon' }, { id: 'orb', label: '💠 Orbit Drones', desc: 'Orbiting drones damage enemies', type: 'weapon' }, { id: 'missile', label: '🚀 Missile Launcher', desc: 'Seeks nearest enemy!', type: 'weapon' }, { id: 'speed', label: '⚡ Speed Boost', desc: 'Move faster', type: 'passive' }, { id: 'hp', label: '❤️ HP Boost', desc: '+30 Max HP & heal some', type: 'passive' }, { id: 'xpRange', label: '🧲 XP Magnet', desc: 'Collect XP from farther away', type: 'passive' }, ]; function getUpgradeChoices() { let pool = ALL_UPGRADES.filter(u => { if (u.type === 'weapon') return (weaponLevels[u.id] || 0) < 5; return (passives[u.id] || 0) < 5; }); for (let i = pool.length-1; i > 0; i--) { let j = Math.floor(Math.random()*(i+1)); [pool[i],pool[j]] = [pool[j],pool[i]]; } return pool.slice(0,3); } function showLevelUp() { levelingUp = true; totalLevelsEver++; refreshUnlocks(); const choices = getUpgradeChoices(); const container = document.getElementById('upgradeOptions'); container.innerHTML = ''; choices.forEach(u => { const btn = document.createElement('button'); btn.className = 'upgrade-btn'; const lvlStr = u.type === 'weapon' ? `Lv${(weaponLevels[u.id]||0)+1}` : `Lv${(passives[u.id]||0)+1}`; btn.innerHTML = `${u.label} <span style="color:#ffdd00">[${lvlStr}]</span><br><small style="color:#aaa">${u.desc}</small>`; btn.onclick = () => applyUpgrade(u.id, u.type); container.appendChild(btn); }); document.getElementById('levelup').style.display = 'block'; } function applyUpgrade(id, type) { if (type === 'weapon') weaponLevels[id] = (weaponLevels[id]||0)+1; else { passives[id] = (passives[id]||0)+1; if (id === 'speed') player.speed += 0.4; if (id === 'hp') { player.maxHp += 30; player.hp = Math.min(player.hp+25, player.maxHp); } } document.getElementById('levelup').style.display = 'none'; levelingUp = false; updateUI(); } function gainXP(amount) { xp += amount; while (xp >= xpNeeded) { xp -= xpNeeded; lvl++; xpNeeded = Math.floor(xpNeeded*1.35); showLevelUp(); } updateUI(); } function updateUI() { document.getElementById('hpText').textContent = Math.ceil(player.hp)+'/'+player.maxHp; document.getElementById('lvlText').textContent = lvl; document.getElementById('killText').textContent = kills; document.getElementById('xpbar').style.width = Math.min(100,(xp/xpNeeded)*100)+'%'; } function updateTimer() { const s = Math.floor(elapsed); const mm = String(Math.floor(s/60)).padStart(2,'0'); const ss = String(s%60).padStart(2,'0'); document.getElementById('timer').textContent = mm+':'+ss; } // ---- Spawning (world coords) ---- function spawnEnemy() { const angle = Math.random()*Math.PI*2; const spawnDist = Math.max(VIEW_W(), VIEW_H())/2 + 80; const wx = player.wx + Math.cos(angle)*spawnDist*(0.8+Math.random()*0.5); const wy = player.wy + Math.sin(angle)*spawnDist*(0.8+Math.random()*0.5); const t = elapsed; const big = t > 90 && Math.random() < 0.2; const fast = !big && t > 60 && Math.random() < 0.2; enemies.push({ wx, wy, r: big ? 22 : (fast ? 10 : 15), hp: big ? 60+Math.floor(t/10)*5 : (fast ? 15 : 25+Math.floor(t/15)*3), maxHp: big ? 60+Math.floor(t/10)*5 : (fast ? 15 : 25+Math.floor(t/15)*3), speed: big ? 0.9 : (fast ? 2.8 : 1.2+Math.min(t/120,1)), big, fast, color: big ? '#ff4444' : (fast ? '#ffaa00' : '#ff6688'), angle: 0, }); } // ---- Weapons ---- let weaponTimers = { laser: 0, orb: 0, missile: 0, orbAngle: 0 }; function updateWeapons(dt) { if (weaponLevels.laser > 0) { const fireRate = Math.max(0.22, 1.0 - weaponLevels.laser*0.14); weaponTimers.laser -= dt; if (weaponTimers.laser <= 0) { weaponTimers.laser = fireRate; fireToNearest(weaponLevels.laser); } } if (weaponLevels.orb > 0) { weaponTimers.orbAngle += dt*2; } if (weaponLevels.missile > 0) { const fireRate = Math.max(1.0, 2.5-weaponLevels.missile*0.3); weaponTimers.missile -= dt; if (weaponTimers.missile <= 0) { weaponTimers.missile = fireRate; fireMissile(weaponLevels.missile); } } } function nearestEnemy() { let closest = null, closestD = Infinity; enemies.forEach(e => { const d = wdist(player, e); if (d < closestD) { closestD = d; closest = e; } }); return closest; } function fireToNearest(wlvl) { const e = nearestEnemy(); if (!e) return; const dx = e.wx-player.wx, dy = e.wy-player.wy; const len = Math.sqrt(dx*dx+dy*dy)||1; const spd = 9; const dmg = 8+wlvl*5; bullets.push({ wx: player.wx, wy: player.wy, vx: dx/len*spd, vy: dy/len*spd, r: 5+wlvl, dmg, color: '#00ffcc', type: 'laser', life: 1.8 }); } function fireMissile(wlvl) { const e = nearestEnemy(); if (!e) return; const dx = e.wx-player.wx, dy = e.wy-player.wy; const len = Math.sqrt(dx*dx+dy*dy)||1; const dmg = 25+wlvl*10; bullets.push({ wx: player.wx, wy: player.wy, vx: dx/len*5, vy: dy/len*5, r: 7, dmg, color: '#ff6600', type: 'missile', target: e, life: 4.0, wlvl }); } function updateOrbDamage() { if (weaponLevels.orb <= 0) return; const count = weaponLevels.orb+1; const orbR = 70+weaponLevels.orb*8; for (let i = 0; i < count; i++) { const angle = weaponTimers.orbAngle+(i/count)*Math.PI*2; const ox = player.wx+Math.cos(angle)*orbR; const oy = player.wy+Math.sin(angle)*orbR; enemies.forEach(e => { if (wdist2(ox,oy,e.wx,e.wy) < e.r+10) { e.hp -= 0.5*(1+weaponLevels.orb*0.3); spawnHitParticle(e.wx, e.wy, '#00aaff'); } }); } } // ---- Main Loop ---- function gameLoop(ts) { if (state !== 'playing') return; if (!lastTime) lastTime = ts; const dt = Math.min((ts-lastTime)/1000, 0.05); lastTime = ts; if (!levelingUp) { elapsed += dt; update(dt); } draw(); requestAnimationFrame(gameLoop); } function update(dt) { updateTimer(); // Player movement (world space, infinite) let dx = 0, dy = 0; if (keys['w']||keys['W']||keys['ArrowUp']) dy -= 1; if (keys['s']||keys['S']||keys['ArrowDown']) dy += 1; if (keys['a']||keys['A']||keys['ArrowLeft']) dx -= 1; if (keys['d']||keys['D']||keys['ArrowRight']) dx += 1; if (dx !== 0 || dy !== 0) { const dlen = Math.sqrt(dx*dx+dy*dy); player.wx += dx/dlen * player.speed * 60 * dt; player.wy += dy/dlen * player.speed * 60 * dt; player.facing = Math.atan2(dy, dx); } player.invincible = Math.max(0, player.invincible-dt); // Smooth camera follow const targetCX = player.wx - VIEW_W()/2; const targetCY = player.wy - VIEW_H()/2; camX += (targetCX - camX) * 0.12; camY += (targetCY - camY) * 0.12; // Spawn enemies spawnTimer -= dt; spawnInterval = Math.max(0.35, 2.0-elapsed*0.005); if (spawnTimer <= 0) { spawnTimer = spawnInterval; const count = 1+Math.floor(elapsed/30); for (let i = 0; i < count; i++) spawnEnemy(); } updateWeapons(dt); updateOrbDamage(); // Bullets bullets.forEach(b => { if (b.type==='missile' && b.target && enemies.includes(b.target)) { const tdx = b.target.wx-b.wx, tdy = b.target.wy-b.wy; const tlen = Math.sqrt(tdx*tdx+tdy*tdy)||1; b.vx += tdx/tlen*0.4; b.vy += tdy/tlen*0.4; const spd = Math.sqrt(b.vx*b.vx+b.vy*b.vy); if (spd > 8) { b.vx = b.vx/spd*8; b.vy = b.vy/spd*8; } } b.wx += b.vx; b.wy += b.vy; b.life -= dt; }); // Bullet vs enemy bullets.forEach(b => { enemies.forEach(e => { if (!b.hit && wdist2(b.wx,b.wy,e.wx,e.wy) < e.r+b.r) { e.hp -= b.dmg; b.hit = true; spawnHitParticle(e.wx, e.wy, b.color); } }); }); bullets = bullets.filter(b => !b.hit && b.life > 0); // Enemy movement enemies.forEach(e => { const edx = player.wx-e.wx, edy = player.wy-e.wy; const elen = Math.sqrt(edx*edx+edy*edy)||1; e.wx += edx/elen*e.speed*60*dt; e.wy += edy/elen*e.speed*60*dt; e.angle += dt*2; if (player.invincible <= 0 && wdist2(player.wx,player.wy,e.wx,e.wy) < player.r+e.r) { const dmg = e.big ? 15 : (e.fast ? 8 : 10); player.hp -= dmg; player.invincible = 0.6; spawnHitParticle(player.wx, player.wy, '#ff0000'); if (player.hp <= 0) { gameOver(); return; } } }); // Enemy death enemies = enemies.filter(e => { if (e.hp <= 0) { kills++; const xpVal = e.big ? 6 : (e.fast ? 2 : 3); xpOrbs.push({ wx: e.wx, wy: e.wy, val: xpVal, r: 7 }); spawnDeathParticles(e.wx, e.wy, e.color); return false; } return true; }); // XP orbs const collectR = 50+(passives.xpRange||0)*25; xpOrbs = xpOrbs.filter(o => { const d = wdist2(player.wx,player.wy,o.wx,o.wy); if (d < collectR) { gainXP(o.val); return false; } if (d < collectR*2.5) { const od = Math.sqrt((player.wx-o.wx)**2+(player.wy-o.wy)**2)||1; o.wx += (player.wx-o.wx)/od*4; o.wy += (player.wy-o.wy)/od*4; } return true; }); // Cull very far enemies & orbs const cullDist = Math.max(VIEW_W(), VIEW_H()) * 2; enemies = enemies.filter(e => wdist2(player.wx, player.wy, e.wx, e.wy) < cullDist); xpOrbs = xpOrbs.filter(o => wdist2(player.wx, player.wy, o.wx, o.wy) < cullDist); // Particles (screen-space stored as world) particles.forEach(p => { p.wx += p.vx; p.wy += p.vy; p.life -= dt; p.vx *= 0.93; p.vy *= 0.93; }); particles = particles.filter(p => p.life > 0); updateUI(); } // ---- World to Screen ---- function wx2s(wx) { return wx - camX; } function wy2s(wy) { return wy - camY; } // ---- Draw ---- function draw() { const VW = VIEW_W(), VH = VIEW_H(); ctx.fillStyle = '#0a0a1a'; ctx.fillRect(0,0,VW,VH); // Infinite scrolling grid ctx.strokeStyle = '#111133'; ctx.lineWidth = 1; const gridSize = 60; const offX = ((camX % gridSize) + gridSize) % gridSize; const offY = ((camY % gridSize) + gridSize) % gridSize; for (let gx = -offX; gx < VW+gridSize; gx += gridSize) { ctx.beginPath(); ctx.moveTo(gx,0); ctx.lineTo(gx,VH); ctx.stroke(); } for (let gy = -offY; gy < VH+gridSize; gy += gridSize) { ctx.beginPath(); ctx.moveTo(0,gy); ctx.lineTo(VW,gy); ctx.stroke(); } // Coordinate label (subtle) ctx.fillStyle = '#ffffff08'; ctx.font = '11px Courier New'; ctx.fillText(`[${Math.floor(player.wx)}, ${Math.floor(player.wy)}]`, 10, VH-20); // XP orbs xpOrbs.forEach(o => { const sx = wx2s(o.wx), sy = wy2s(o.wy); if (sx < -20 || sx > VW+20 || sy < -20 || sy > VH+20) return; ctx.beginPath(); ctx.arc(sx, sy, o.r, 0, Math.PI*2); ctx.fillStyle = '#00ff88'; ctx.shadowColor = '#00ff88'; ctx.shadowBlur = 10; ctx.fill(); ctx.shadowBlur = 0; }); // Particles particles.forEach(p => { const sx = wx2s(p.wx), sy = wy2s(p.wy); ctx.globalAlpha = Math.max(0, p.life); ctx.beginPath(); ctx.arc(sx, sy, p.r, 0, Math.PI*2); ctx.fillStyle = p.color; ctx.fill(); }); ctx.globalAlpha = 1; // Enemies enemies.forEach(e => { const sx = wx2s(e.wx), sy = wy2s(e.wy); if (sx < -60 || sx > VW+60 || sy < -60 || sy > VH+60) return; ctx.save(); ctx.translate(sx, sy); ctx.rotate(e.angle); ctx.fillStyle = e.color; ctx.shadowColor = e.color; ctx.shadowBlur = 12; if (e.big) { ctx.fillRect(-e.r,-e.r,e.r*2,e.r*2); ctx.fillStyle='#ffaaaa'; ctx.fillRect(-e.r+4,-e.r+4,8,8); ctx.fillRect(e.r-12,-e.r+4,8,8); } else if (e.fast) { ctx.beginPath(); ctx.moveTo(0,-e.r); ctx.lineTo(e.r,e.r); ctx.lineTo(-e.r,e.r); ctx.closePath(); ctx.fill(); } else { ctx.fillRect(-e.r,-e.r,e.r*2,e.r*2); ctx.fillStyle='#330000'; ctx.fillRect(-5,-8,4,6); ctx.fillRect(1,-8,4,6); } ctx.shadowBlur = 0; // hp bar ctx.fillStyle='#333'; ctx.fillRect(-e.r,-e.r-8,e.r*2,4); ctx.fillStyle='#ff3333'; ctx.fillRect(-e.r,-e.r-8,e.r*2*(e.hp/e.maxHp),4); ctx.restore(); }); // Orbit drones if (weaponLevels.orb > 0) { const count = weaponLevels.orb+1; const orbR = 70+weaponLevels.orb*8; const psx = wx2s(player.wx), psy = wy2s(player.wy); for (let i = 0; i < count; i++) { const angle = weaponTimers.orbAngle+(i/count)*Math.PI*2; const ox = psx+Math.cos(angle)*orbR; const oy = psy+Math.sin(angle)*orbR; ctx.beginPath(); ctx.arc(ox,oy,9,0,Math.PI*2); ctx.fillStyle='#00aaff'; ctx.shadowColor='#00aaff'; ctx.shadowBlur=14; ctx.fill(); ctx.shadowBlur=0; } } // Bullets bullets.forEach(b => { const sx = wx2s(b.wx), sy = wy2s(b.wy); ctx.beginPath(); ctx.arc(sx,sy,b.r,0,Math.PI*2); ctx.fillStyle = b.color; ctx.shadowColor = b.color; ctx.shadowBlur = 14; ctx.fill(); ctx.shadowBlur = 0; if (b.type==='missile') { ctx.fillStyle='#ffaa00'; ctx.beginPath(); ctx.arc(sx,sy,b.r-2,0,Math.PI*2); ctx.fill(); } }); // Player (drawn at screen center-ish, follows camera) const psx = wx2s(player.wx), psy = wy2s(player.wy); ctx.save(); ctx.translate(psx, psy); if (player.invincible > 0) ctx.globalAlpha = 0.4+Math.sin(Date.now()*0.03)*0.3; drawRobotPlayer(ctx, currentChar); ctx.globalAlpha = 1; ctx.shadowBlur = 0; ctx.restore(); // HP bar above player const hpW = 130, hpH = 12; const hpX = psx-hpW/2, hpY = psy+player.r+10; ctx.fillStyle='#330000'; ctx.fillRect(hpX,hpY,hpW,hpH); ctx.fillStyle=`hsl(${(player.hp/player.maxHp)*120},100%,50%)`; ctx.fillRect(hpX,hpY,hpW*(player.hp/player.maxHp),hpH); ctx.strokeStyle='#ffffff33'; ctx.lineWidth=1; ctx.strokeRect(hpX,hpY,hpW,hpH); // Minimap drawMinimap(); } function drawRobotPlayer(c2d, charDef) { c2d.shadowColor = charDef.color; c2d.shadowBlur = 20; if (charDef.id === 'scout') { c2d.fillStyle = charDef.bodyColor; c2d.fillRect(-12,-10,24,22); c2d.fillStyle = '#aaffee'; c2d.fillRect(-8,-18,16,12); c2d.fillStyle = charDef.eyeColor; c2d.fillRect(-5,-15,4,5); c2d.fillRect(1,-15,4,5); c2d.fillStyle = '#00ffaa'; c2d.fillRect(-4,-14,2,3); c2d.fillRect(2,-14,2,3); c2d.fillStyle = '#00bbaa'; c2d.fillRect(-10,12,7,8); c2d.fillRect(3,12,7,8); } else if (charDef.id === 'tank') { c2d.fillStyle = charDef.bodyColor; c2d.fillRect(-16,-12,32,26); c2d.fillStyle = '#ffccaa'; c2d.fillRect(-12,-22,24,14); c2d.fillStyle = charDef.eyeColor; c2d.fillRect(-8,-18,6,6); c2d.fillRect(2,-18,6,6); c2d.fillStyle = '#ff9944'; c2d.fillRect(-7,-17,3,4); c2d.fillRect(3,-17,3,4); c2d.fillStyle = '#cc4422'; c2d.fillRect(-18,-12,6,26); c2d.fillRect(12,-12,6,26); c2d.fillStyle = charDef.bodyColor; c2d.fillRect(-14,14,11,10); c2d.fillRect(3,14,11,10); } else if (charDef.id === 'sniper') { c2d.fillStyle = charDef.bodyColor; c2d.fillRect(-10,-9,20,22); c2d.fillStyle = '#ddaaff'; c2d.fillRect(-8,-18,16,12); c2d.fillStyle = charDef.eyeColor; c2d.fillRect(-5,-15,3,5); c2d.fillRect(2,-15,3,5); c2d.fillStyle = '#cc88ff'; c2d.fillRect(-4,-14,2,3); c2d.fillRect(3,-14,2,3); c2d.fillStyle = '#9955ee'; c2d.fillRect(9,-4,18,5); c2d.fillStyle = charDef.bodyColor; c2d.fillRect(-8,13,6,9); c2d.fillRect(2,13,6,9); } } // ---- Minimap ---- function drawMinimap() { const MW = miniCanvas.width, MH = miniCanvas.height; const scale = 0.04; mctx.fillStyle = 'rgba(0,0,0,0.75)'; mctx.fillRect(0,0,MW,MH); mctx.strokeStyle = '#00ffcc44'; mctx.strokeRect(0,0,MW,MH); const cx = MW/2, cy = MH/2; // enemies enemies.forEach(e => { const mx = cx+(e.wx-player.wx)*scale; const my = cy+(e.wy-player.wy)*scale; if (mx<0||mx>MW||my<0||my>MH) return; mctx.fillStyle = e.color; mctx.fillRect(mx-2,my-2,4,4); }); // xp orbs xpOrbs.forEach(o => { const mx = cx+(o.wx-player.wx)*scale; const my = cy+(o.wy-player.wy)*scale; if (mx<0||mx>MW||my<0||my>MH) return; mctx.fillStyle = '#00ff88'; mctx.fillRect(mx-1,my-1,3,3); }); // player mctx.fillStyle = currentChar ? currentChar.color : '#00ffcc'; mctx.beginPath(); mctx.arc(cx,cy,4,0,Math.PI*2); mctx.fill(); } // ---- Particles ---- function spawnHitParticle(wx, wy, color) { for (let i=0;i<5;i++) { const a=Math.random()*Math.PI*2; particles.push({wx,wy,vx:Math.cos(a)*2.5*Math.random(),vy:Math.sin(a)*2.5*Math.random(),r:3+Math.random()*3,color,life:0.5+Math.random()*0.3}); } } function spawnDeathParticles(wx, wy, color) { for (let i=0;i<14;i++) { const a=Math.random()*Math.PI*2; particles.push({wx,wy,vx:Math.cos(a)*5*Math.random(),vy:Math.sin(a)*5*Math.random(),r:4+Math.random()*4,color,life:0.9+Math.random()*0.5}); } } // ---- Helpers ---- function wdist(a,b) { return Math.sqrt((a.wx-b.wx)**2+(a.wy-b.wy)**2); } function wdist2(x1,y1,x2,y2) { return Math.sqrt((x1-x2)**2+(y1-y2)**2); } // ---- Game Over ---- function gameOver() { state = 'menu'; const overlay = document.getElementById('overlay'); overlay.innerHTML = ` <h1 style="font-size:40px;color:#ff4444;text-shadow:0 0 20px #ff4444;">💀 GAME OVER</h1> <p style="font-size:20px;color:#ffdd00;margin:12px;">Survived: <b>${document.getElementById('timer').textContent}</b></p> <p style="font-size:18px;color:#aaffee;">Kills: <b>${kills}</b> Level: <b>${lvl}</b></p> <p style="font-size:14px;color:#888;margin-top:6px;">Session total levels: ${totalLevelsEver}</p> <button id="startBtn" style="margin-top:28px;padding:14px 40px;font-size:20px;background:#ff4444;color:#fff;border:none;border-radius:8px;cursor:pointer;font-family:'Courier New',monospace;font-weight:bold;box-shadow:0 0 20px #ff4444;">PLAY AGAIN</button> `; overlay.style.display = 'flex'; document.getElementById('startBtn').onclick = () => showMenu(); } function showMenu() { const overlay = document.getElementById('overlay'); overlay.innerHTML = ` <h1>🤖 ROBOT SURVIVOR</h1> <p>Move: <b>WASD</b> or <b>Arrow Keys</b> | World is <b>infinite</b>!</p> <p style="color:#ffdd00;margin-top:4px;">Choose your Robot:</p> <div class="char-select" id="charSelect"></div> <div id="charDesc" style="color:#aaffee;font-size:14px;min-height:20px;"></div> <button id="startBtn">START GAME</button> <p id="unlockHint" style="margin-top:12px;font-size:13px;color:#888;"></p> `; overlay.style.display = 'flex'; buildCharSelect(); document.getElementById('startBtn').onclick = startGame; } function startGame() { if (!unlockedChars.has(selectedCharId)) { selectedCharId = 'scout'; } document.getElementById('overlay').style.display = 'none'; initGame(); state = 'playing'; lastTime = null; requestAnimationFrame(gameLoop); } // ---- Boot ---- buildCharSelect(); document.getElementById('startBtn').onclick = startGame; </script> </body> </html>
PREVIEW
PICTURES0/50
No pictures yet. Add one, then ask your buddy to put it in your app!