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!