Spaces:
Running
Running
| <html> | |
| <head> | |
| <title>Neon Nexus Survivor 2.2</title> | |
| <style> | |
| body { margin: 0; overflow: hidden; background: #080808; font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; user-select: none; } | |
| canvas { display: block; filter: drop-shadow(0 0 10px rgba(0,255,255,0.3)); } | |
| #ui-layer { position: absolute; top: 0; left: 0; width: 100%; height: 100%; pointer-events: none; display: flex; flex-direction: column; justify-content: space-between; padding: 20px; box-sizing: border-box; z-index: 400; } | |
| .hud-top { display: flex; gap: 20px; align-items: flex-start; } | |
| .stat-box { display: flex; flex-direction: column; gap: 8px; } | |
| .bar-wrap { width: 300px; height: 20px; background: rgba(50,50,50,0.6); border: 1px solid rgba(255,255,255,0.25); border-radius: 6px; overflow: hidden; position: relative; } | |
| .bar-fill { height: 100%; transition: width 0.12s linear; } | |
| #hp-fill { background: linear-gradient(90deg, #ff7b7b, #ff0000); width: 100%; } | |
| #xp-fill { background: linear-gradient(90deg, #7bff7b, #00ff00); width: 0%; } | |
| #rage-fill { background: linear-gradient(90deg, #ffd27b, #ff8800); width: 0%; box-shadow: 0 0 10px orange; } | |
| .label { font-size: 12px; color: #ddd; font-weight: bold; position: absolute; top: 2px; left: 8px; text-shadow: 1px 1px 0 #000; pointer-events: none; } | |
| .bar-value { position: absolute; right: 8px; top: 1px; font-size: 12px; color: #fff; font-weight: 700; text-shadow: 0 0 6px rgba(0,0,0,0.8); pointer-events: none; } | |
| .big-text { font-size: 34px; font-weight: 900; color: white; text-shadow: 0 0 15px cyan; margin-bottom: 5px; letter-spacing: 2px; } | |
| .stat-row { display:flex; align-items:center; gap:8px; } | |
| #cooldowns { text-align: right; color: cyan; font-family: monospace; font-size: 16px; text-shadow: 0 0 5px cyan; pointer-events: none; } | |
| .cd-ready { color: #00ff00; } | |
| .cd-wait { color: #ff3333; } | |
| #upgrade-overlay { position: absolute; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.8); backdrop-filter: blur(5px); display: none; align-items: center; justify-content: center; z-index: 1000; pointer-events: auto; } | |
| #cards-container { display: flex; gap: 30px; } | |
| .card { width: 220px; height: 320px; background: linear-gradient(145deg, #111, #222); border: 2px solid #333; border-radius: 15px; display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 20px; cursor: pointer; transition: transform 0.2s, border-color 0.2s, box-shadow 0.2s; position: relative; overflow: hidden; text-align: center; color: white; pointer-events: auto; } | |
| .card:hover { transform: translateY(-10px) scale(1.05); border-color: cyan; box-shadow: 0 0 30px rgba(0,255,255,0.4); } | |
| .card h3 { color: cyan; margin: 0 0 10px 0; font-size: 20px; text-transform: uppercase; } | |
| .card p { font-size: 13px; color: #ccc; line-height: 1.4; } | |
| .card .rarity { position: absolute; top: 10px; right: 10px; width: 12px; height: 12px; border-radius: 50%; background: cyan; box-shadow: 0 0 10px cyan; } | |
| #start-screen, #death-screen { position: absolute; top: 0; left: 0; width: 100%; height: 100%; background: #050505; display: flex; flex-direction: column; align-items: center; justify-content: center; z-index: 900; color: white; pointer-events: auto; } | |
| h1 { font-size: 80px; margin: 0; background: linear-gradient(to bottom, #fff, #888); -webkit-background-clip: text; -webkit-text-fill-color: transparent; filter: drop-shadow(0 0 20px cyan); letter-spacing: 5px; } | |
| button.btn-main { padding: 16px 48px; font-size: 20px; background: transparent; color: cyan; border: 3px solid cyan; font-weight: bold; cursor: pointer; text-transform: uppercase; margin-top: 24px; transition: 0.25s; letter-spacing: 3px; position: relative; overflow: hidden; pointer-events: auto; } | |
| button.btn-main:hover { background: cyan; color: black; box-shadow: 0 0 50px cyan; } | |
| #hint-box { font-family: monospace; color: rgba(255,255,255,0.7); text-align: right; margin-top: auto; pointer-events: none; } | |
| #intensity-display { font-family: monospace; color: #ff8; margin-top: 6px; text-shadow: 0 0 6px rgba(0,0,0,0.8); } | |
| </style> | |
| </head> | |
| <body> | |
| <div id="ui-layer"> | |
| <div class="hud-top"> | |
| <div class="stat-box"> | |
| <div class="big-text" id="matter-display">0</div> | |
| <div class="bar-wrap"><div class="label">HP</div><div class="bar-fill" id="hp-fill"></div><div class="bar-value" id="hp-value">100 / 100</div></div> | |
| <div class="bar-wrap"><div class="label">XP</div><div class="bar-fill" id="xp-fill"></div><div class="bar-value" id="xp-value">0 / 100</div></div> | |
| <div class="bar-wrap"><div class="label">RAGE</div><div class="bar-fill" id="rage-fill"></div><div class="bar-value" id="rage-value">0%</div></div> | |
| <div style="display:flex; gap:10px; margin-top:8px; align-items:center;"> | |
| <div style="color:#ccc; font-weight:700;">LVL <span id="lvl-display">1</span></div> | |
| <div style="color:#ccc;">SCORE <span id="score-display">0</span></div> | |
| </div> | |
| </div> | |
| </div> | |
| <div id="cooldowns"> | |
| <div id="cd-dash">DASH [SPACE]</div> | |
| <div id="cd-slow">SLOW [SHIFT]</div> | |
| <div id="cd-build">BUILD [B] (50 Matter)</div> | |
| </div> | |
| <div id="hint-box"> | |
| WAVE <span id="wave-num">1</span> <br> | |
| TIME <span id="game-time">0:00</span> | |
| <div id="intensity-display">INTENSITY: 1.00</div> | |
| </div> | |
| </div> | |
| <div id="upgrade-overlay"> | |
| <div id="cards-container"></div> | |
| </div> | |
| <div id="start-screen"> | |
| <h1>NEON NEXUS</h1> | |
| <p style="color:#888; letter-spacing:2px; margin-bottom: 20px;">SYSTEM 2.2 ONLINE</p> | |
| <button class="btn-main" onclick="initGame()">ENGAGE</button> | |
| </div> | |
| <div id="death-screen" style="display:none;"> | |
| <h1 style="filter: drop-shadow(0 0 20px red); -webkit-text-fill-color: red;">CRITICAL ERROR</h1> | |
| <h2 id="final-score">SCORE: 0</h2> | |
| <button class="btn-main" onclick="location.reload()">REBOOT</button> | |
| </div> | |
| <canvas id="cvs"></canvas> | |
| <script> | |
| const cvs = document.getElementById('cvs'); | |
| const ctx = cvs.getContext('2d'); | |
| let W, H; | |
| let loopId; | |
| let frame = 0; | |
| let timeSeconds = 0; | |
| let score = 0; | |
| let shake = 0; | |
| let intensity = 1; | |
| // Movement tuning | |
| const PLAYER_BASE_SPEED = 5.2; // increased base | |
| const PLAYER_ACCEL = 0.26; // smoothing | |
| const PLAYER_FRICTION = 0.06; // soft friction | |
| const DASH_IMPULSE = 60; // big dash impulse | |
| const DASH_COOLDOWN_FRAMES = 80; // cooldown length | |
| const keys = {}; | |
| const mouse = { x:0, y:0 }; | |
| let gameState = 'START'; | |
| const UPGRADES_POOL = [ | |
| { id: 'spread', name: 'Scatter Shot', desc: '+1 Projectile count', type: 'atk' }, | |
| { id: 'rate', name: 'Hyper Loader', desc: '-3 Fire Rate (lower is faster)', type: 'atk' }, | |
| { id: 'dmg', name: 'Plasma Core', desc: '+25% Damage', type: 'atk' }, | |
| { id: 'speed', name: 'Thrusters', desc: '+15% Move Speed', type: 'util' }, | |
| { id: 'hp', name: 'Nano Hull', desc: '+50 Max HP & Heal', type: 'def' }, | |
| { id: 'magnet', name: 'Graviton', desc: '+50% Pickup Range', type: 'util' }, | |
| { id: 'aura_unlock', name: 'Void Aura', desc: 'Unlock a damaging field', type: 'aura', req: '!aura' }, | |
| { id: 'aura_dmg', name: 'Void Intensity', desc: '+50% Aura Damage', type: 'aura', req: 'aura' }, | |
| { id: 'aura_range', name: 'Void Expanse', desc: '+30% Aura Range', type: 'aura', req: 'aura' }, | |
| { id: 'drone_cnt', name: 'Drone Fab', desc: 'Add +1 Orbiting Drone', type: 'drone' }, | |
| { id: 'rage_cap', name: 'Adrenaline', desc: 'Rage charges faster', type: 'util' }, | |
| // New cards | |
| { id: 'regen', name: 'Auto-Repair', desc: 'Regenerate 1 HP per 2s', type: 'def' }, | |
| { id: 'explosion', name: 'Demolition', desc: 'Enemies explode on death (AOE)', type: 'atk' }, | |
| { id: 'crit', name: 'Overclock', desc: 'Small chance to crit for 2x', type: 'atk' }, | |
| { id: 'shield', name: 'Reactive Shield', desc: 'Gain a short shield on hits', type: 'def' }, | |
| { id: 'boss_hunter', name: 'Hunter Protocol', desc: '+20% damage vs bosses', type: 'atk' }, | |
| ]; | |
| let player = { | |
| x:0, y:0, r:12, color:'#0ff', | |
| vx:0, vy:0, speed:PLAYER_BASE_SPEED, | |
| hp:100, maxHp:100, | |
| xp:0, nextLvl:100, lvl:1, | |
| rage:0, maxRage:100, rageActive:false, rageTimer:0, | |
| dash:0, dashMax:DASH_COOLDOWN_FRAMES, | |
| slow:0, slowMax:300, slowActive:false, slowDuration:0, | |
| matter:0, | |
| stats: { spread:1, rate:20, dmg:10, pickup:80, auraDmg:0.5, auraRange:120, regen:false, explosion:false, critChance:0.05, bossDmgBoost:1.0 }, | |
| auraUnlocked: false, | |
| trails: [], | |
| invincible: 0, | |
| dashing: false | |
| }; | |
| let drones = []; | |
| let bullets = []; | |
| let enemies = []; | |
| let particles = []; | |
| let texts = []; | |
| let ores = []; | |
| let turrets = []; | |
| let pickups = []; | |
| const FACE_VARIANTS = [ 'ಠ_ಠ', 'ᕦ(ò_óˇ)ᕤ', '⊙﹏⊙', '☠', '▣_▣', '▓▒░', '¬_¬', 'ʘ‿ʘ', '(╬ಠ益ಠ)' ]; | |
| function resize(){ | |
| W = cvs.width = window.innerWidth; | |
| H = cvs.height = window.innerHeight; | |
| } | |
| window.onresize = resize; | |
| window.onmousemove = e => { mouse.x = e.clientX; mouse.y = e.clientY; }; | |
| window.onkeydown = e => keys[e.code] = true; | |
| window.onkeyup = e => keys[e.code] = false; | |
| function rnd(min,max){ return Math.random()*(max-min)+min; } | |
| function irnd(max){ return Math.floor(Math.random()*max); } | |
| function choose(arr){ return arr[Math.floor(Math.random()*arr.length)]; } | |
| function initGame(){ | |
| resize(); | |
| document.getElementById('start-screen').style.display = 'none'; | |
| player.x = W/2; player.y = H/2; | |
| player.vx = player.vy = 0; | |
| player.dash = 0; | |
| gameState = 'PLAY'; | |
| drones = []; bullets = []; enemies = []; particles = []; texts = []; ores = []; turrets = []; pickups = []; | |
| addDrone(); | |
| for(let i=0; i<6; i++) spawnOre(); | |
| frame = 0; timeSeconds = 0; score = 0; intensity = 1; | |
| renderLoop(); | |
| } | |
| function addDrone(){ | |
| drones.push({ angle: (Math.PI*2/ (drones.length+1)) * drones.length, dist: 70, r:6, lastShot:0 }); | |
| recalcDrones(); | |
| } | |
| function recalcDrones(){ | |
| drones.forEach((d,i) => { | |
| d.targetAngleOffset = (Math.PI*2 / drones.length) * i; | |
| }); | |
| } | |
| function spawnOre(){ | |
| ores.push({ x: rnd(100, W-100), y: rnd(100, H-100), r: rnd(25, 35), hp:150, max:150 }); | |
| } | |
| function spawnEnemy(forceType){ | |
| let side = Math.floor(rnd(0,4)); | |
| let x,y; | |
| if(side===0){ x=rnd(0,W); y=-60; } | |
| else if(side===1){ x=W+60; y=rnd(0,H); } | |
| else if(side===2){ x=rnd(0,W); y=H+60; } | |
| else { x=-60; y=rnd(0,H); } | |
| // intensity scales over time and score (affects spawn count/hp, NOT speed) | |
| intensity = 1 + (timeSeconds/120) + (score/800); | |
| let dif = 1 + (timeSeconds/60) * 0.5 * Math.min(3, intensity/2); | |
| let typeRoll = forceType || rnd(0,100); | |
| // base speed is fixed per type (no intensity multiplier) | |
| let baseSpd = rnd(1.4, 2.2); | |
| let e = { x, y, vx:0, vy:0, r:15, hp:20*dif, max:20*dif, baseSpd: baseSpd, spd: baseSpd, type:'grunt', color:'#f0f', enraged:false, face: choose(FACE_VARIANTS) }; | |
| if(typeRoll > 95 && timeSeconds > 25){ | |
| // powerful tank-like | |
| e.type = 'tank'; e.r=36; e.hp*=4; e.spd = 0.6; e.baseSpd = e.spd; e.color='#fc0'; | |
| } else if (typeRoll > 80 && timeSeconds > 50){ | |
| e.type = 'rusher'; e.r=12; e.hp*=0.8; e.spd = 2.6; e.baseSpd = e.spd; e.color='#f33'; | |
| } | |
| // Mini-boss spawn more often as intensity rises | |
| if(timeSeconds > 20 && Math.random() < Math.min(0.12, intensity*0.01)){ | |
| e.type = 'mini_boss'; e.r = 48; e.hp = 200 * dif; e.spd = 0.9; e.baseSpd = e.spd; e.color = '#9f0'; | |
| e.face = '(•̀o•́)ง'; | |
| } | |
| // Heavy Boss Spawn (periodic) | |
| if(timeSeconds > 10 && timeSeconds % 45 === 0 && enemies.filter(z=>z.type==='boss').length === 0){ | |
| e.type = 'boss'; e.r = 90; e.hp = 1200 * dif; e.spd = 0.6; e.baseSpd = e.spd; e.color = '#fff'; | |
| e.x = W/2; e.y = -200; e.face = '(╬ಠ益ಠ)'; | |
| screenshake(30); | |
| spawnText('BOSS APPROACH', W/2, 80, '#fff'); | |
| } | |
| enemies.push(e); | |
| } | |
| function screenshake(amt){ shake = Math.max(shake, amt); } | |
| function getUpgradeOptions(){ | |
| let opts = []; | |
| let pool = UPGRADES_POOL.filter(u => { | |
| if(u.req === '!aura' && player.auraUnlocked) return false; | |
| if(u.req === 'aura' && !player.auraUnlocked) return false; | |
| return true; | |
| }); | |
| for(let i=0; i<3; i++){ | |
| let u = pool[Math.floor(Math.random()*pool.length)]; | |
| opts.push(u); | |
| } | |
| return opts; | |
| } | |
| function levelUp(){ | |
| gameState = 'PAUSE'; | |
| document.getElementById('upgrade-overlay').style.display = 'flex'; | |
| let con = document.getElementById('cards-container'); | |
| con.innerHTML = ''; | |
| let opts = getUpgradeOptions(); | |
| opts.forEach(u => { | |
| let c = document.createElement('div'); | |
| c.className = 'card'; | |
| c.innerHTML = `<div class="rarity"></div><h3>${u.name}</h3><p>${u.desc}</p>`; | |
| c.onclick = () => applyUpgrade(u); | |
| con.appendChild(c); | |
| }); | |
| } | |
| function applyUpgrade(u){ | |
| if(u.id === 'spread') player.stats.spread++; | |
| if(u.id === 'rate') player.stats.rate = Math.max(5, player.stats.rate - 3); | |
| if(u.id === 'dmg') player.stats.dmg *= 1.25; | |
| if(u.id === 'speed') player.speed *= 1.15; | |
| if(u.id === 'hp') { player.maxHp+=50; player.hp+=50; } | |
| if(u.id === 'magnet') player.stats.pickup *= 1.5; | |
| if(u.id === 'aura_unlock') player.auraUnlocked = true; | |
| if(u.id === 'aura_dmg') player.stats.auraDmg *= 1.5; | |
| if(u.id === 'aura_range') player.stats.auraRange *= 1.3; | |
| if(u.id === 'drone_cnt') addDrone(); | |
| if(u.id === 'rage_cap') player.maxRage = Math.max(50, player.maxRage - 10); | |
| if(u.id === 'regen') player.stats.regen = true; | |
| if(u.id === 'explosion') player.stats.explosion = true; | |
| if(u.id === 'crit') player.stats.critChance = Math.min(0.4, player.stats.critChance + 0.08); | |
| if(u.id === 'shield') player.shield = (player.shield || 0) + 30; | |
| if(u.id === 'boss_hunter') player.stats.bossDmgBoost *= 1.2; | |
| player.lvl++; | |
| player.xp = 0; | |
| player.nextLvl = Math.floor(player.nextLvl * 1.25); | |
| document.getElementById('upgrade-overlay').style.display = 'none'; | |
| gameState = 'PLAY'; | |
| } | |
| function update(){ | |
| if(shake > 0) shake *= 0.9; | |
| if(frame%60 === 0) timeSeconds++; | |
| let timeMod = player.slowActive ? 0.35 : 1.0; | |
| let rageMod = player.rageActive ? 2.0 : 1.0; // Dmg multiplier | |
| // Improved Player Move (normalized input & smooth acceleration) | |
| let inX=0, inY=0; | |
| if(keys['KeyW']) inY=-1; | |
| if(keys['KeyS']) inY=1; | |
| if(keys['KeyA']) inX=-1; | |
| if(keys['KeyD']) inX=1; | |
| // normalize | |
| let inLen = Math.hypot(inX, inY); | |
| let nx = 0, ny = 0; | |
| if(inLen > 0){ nx = inX / inLen; ny = inY / inLen; } | |
| // Dash (single press detection, bigger and snappier) | |
| if(keys['Space'] && player.dash <= 0){ | |
| player.dash = player.dashMax; | |
| player.dashing = true; | |
| let angle; | |
| if(inLen === 0) angle = Math.atan2(mouse.y-player.y, mouse.x-player.x); | |
| else angle = Math.atan2(ny, nx); | |
| // add impulse on top of current velocity for smoother feel | |
| player.vx += Math.cos(angle)*DASH_IMPULSE; | |
| player.vy += Math.sin(angle)*DASH_IMPULSE; | |
| player.invincible = 22; | |
| screenshake(10); | |
| // afterimages | |
| for(let i=0;i<6;i++) player.trails.push({x:player.x - Math.cos(angle)*i*6, y:player.y - Math.sin(angle)*i*6, alpha:0.8 - i*0.12}); | |
| } | |
| if(player.dash>0) { player.dash--; if(player.dash === 0) player.dashing = false; } | |
| // Slow Time | |
| if(keys['ShiftLeft'] && player.slow <= 0){ | |
| player.slowActive = true; | |
| player.slow = player.slowMax; | |
| player.slowDuration = 90; // frames | |
| } | |
| if(player.slowActive && player.slowDuration > 0) player.slowDuration--; | |
| if(player.slowActive && player.slowDuration <= 0) player.slowActive = false; | |
| if(!player.slowActive && player.slow>0) player.slow--; | |
| // Build | |
| if(keys['KeyB'] && player.matter >= 50){ | |
| keys['KeyB'] = false; | |
| player.matter -= 50; | |
| turrets.push({x:player.x, y:player.y, cd:0}); | |
| spawnText("TURRET READY", player.x, player.y-20, '#0f0'); | |
| } | |
| // Physics - smooth approach to desired velocity | |
| let maxSpeed = player.speed * (player.rageActive ? 1.5 : 1); | |
| let dvx = nx * maxSpeed; | |
| let dvy = ny * maxSpeed; | |
| player.vx += (dvx - player.vx) * PLAYER_ACCEL; | |
| player.vy += (dvy - player.vy) * PLAYER_ACCEL; | |
| // friction & invincibility behavior | |
| if(player.invincible > 0){ player.vx *= 0.92; player.vy *= 0.92; player.invincible--; } | |
| else { player.vx *= (1 - PLAYER_FRICTION); player.vy *= (1 - PLAYER_FRICTION); } | |
| player.x += player.vx * (timeMod); | |
| player.y += player.vy * (timeMod); | |
| player.x = Math.max(0, Math.min(W, player.x)); | |
| player.y = Math.max(0, Math.min(H, player.y)); | |
| // Trails cleanup | |
| if(frame%3===0 && (Math.abs(player.vx)>0.8 || Math.abs(player.vy)>0.8)){ | |
| player.trails.push({x:player.x, y:player.y, alpha:0.6}); | |
| } | |
| for(let i=player.trails.length-1;i>=0;i--){ let t=player.trails[i]; t.alpha -= 0.05; if(t.alpha<=0) player.trails.splice(i,1); } | |
| // Rage | |
| if(player.rage >= player.maxRage && !player.rageActive){ | |
| player.rageActive = true; | |
| player.rageTimer = 300; | |
| screenshake(12); | |
| spawnText("RAGE UNLEASHED", player.x, player.y-40, "orange"); | |
| } | |
| if(player.rageActive){ | |
| player.rageTimer--; | |
| timeMod *= 0.5; // Rage slows enemies relatively | |
| if(player.rageTimer<=0) { player.rageActive=false; player.rage=0; } | |
| } | |
| // Aura Logic | |
| if(player.auraUnlocked){ | |
| enemies.forEach(e => { | |
| let d = Math.hypot(e.x-player.x, e.y-player.y); | |
| if(d < player.stats.auraRange){ | |
| if(frame%10 === 0) { | |
| e.hp -= player.stats.auraDmg * (player.rageActive?2:1); | |
| particles.push({x:e.x, y:e.y, vx:0, vy:-1, life:20, c:'#f0f', s:2}); | |
| if(frame%30===0) spawnText(Math.floor(player.stats.auraDmg), e.x, e.y, '#f0f'); | |
| } | |
| } | |
| }); | |
| } | |
| // Shoot | |
| let fireRate = player.rageActive ? 5 : player.stats.rate; | |
| if(frame % Math.max(1, Math.floor(fireRate)) === 0){ | |
| for(let i=0; i<player.stats.spread * (player.rageActive?2:1); i++){ | |
| let ang = Math.atan2(mouse.y-player.y, mouse.x-player.x); | |
| ang += (Math.random()-0.5)*0.18; | |
| let dmg = player.stats.dmg * rageMod; | |
| // crit check | |
| if(Math.random() < (player.stats.critChance||0)) dmg *= 2; | |
| bullets.push({x:player.x, y:player.y, vx:Math.cos(ang)*12, vy:Math.sin(ang)*12, life:50, dmg:dmg, c:player.rageActive?'orange':'cyan'}); | |
| } | |
| } | |
| // Drones | |
| drones.forEach((d, i) => { | |
| d.angle += 0.05; | |
| d.x = player.x + Math.cos(d.angle + d.targetAngleOffset) * d.dist; | |
| d.y = player.y + Math.sin(d.angle + d.targetAngleOffset) * d.dist; | |
| if(frame % 40 === 0){ | |
| let target = null; let minDist = 9999; | |
| enemies.forEach(e => { let dist = Math.hypot(e.x-d.x, e.y-d.y); if(dist < minDist){ minDist=dist; target=e; }}); | |
| if(target && minDist < 360){ let ang = Math.atan2(target.y-d.y, target.x-d.x); bullets.push({x:d.x, y:d.y, vx:Math.cos(ang)*10, vy:Math.sin(ang)*10, life:40, dmg:5*player.lvl, c:'lime'}); } | |
| } | |
| }); | |
| // Turrets | |
| turrets.forEach(t => { | |
| if(frame%30===0){ | |
| let target = null; let minDist = 250; | |
| enemies.forEach(e => { let d = Math.hypot(e.x-t.x, e.y-t.y); if(d < minDist){ minDist=d; target=e; }}); | |
| if(target){ let ang = Math.atan2(target.y-t.y, target.x-t.x); bullets.push({x:t.x, y:t.y, vx:Math.cos(ang)*15, vy:Math.sin(ang)*15, life:60, dmg:15, c:'yellow'}); } | |
| } | |
| }); | |
| // Bullets Move | |
| for(let i=bullets.length-1;i>=0;i--){ let b=bullets[i]; b.x += b.vx * timeMod; b.y += b.vy * timeMod; b.life--; if(b.life<=0) bullets.splice(i,1); else { if(Math.random()>0.6) particles.push({x:b.x, y:b.y, vx:0, vy:0, life:10, c:b.c, s:2}); }} | |
| // Spawn Logic (more intense over time, but NOT increasing speed) | |
| if(frame % Math.max(8, Math.floor(60 / Math.min(3, 1 + intensity/2))) === 0){ spawnEnemy(); } | |
| // Enemy Logic - smoother steering, fixed speed | |
| for(let i=enemies.length-1;i>=0;i--){ let e = enemies[i]; | |
| let ang = Math.atan2(player.y-e.y, player.x-e.x); | |
| // smoothly approach desired velocity | |
| let targetVx = Math.cos(ang) * e.spd * (player.slowActive ? 0.35 : 1); | |
| let targetVy = Math.sin(ang) * e.spd * (player.slowActive ? 0.35 : 1); | |
| e.vx += (targetVx - e.vx) * 0.06; | |
| e.vy += (targetVy - e.vy) * 0.06; | |
| // Soft Collision between enemies | |
| enemies.forEach(o => { if(e===o) return; let dx = e.x-o.x, dy = e.y-o.y; let d = Math.hypot(dx,dy); if(d < e.r+o.r){ e.vx += dx*0.03; e.vy += dy*0.03; }}); | |
| e.x += e.vx; e.y += e.vy; | |
| // Boss special attacks | |
| if(e.type === 'boss' && frame % 90 === 0){ | |
| for(let a=0;a<12;a++){ let ang2 = a*(Math.PI*2/12) + (frame*0.02); bullets.push({x:e.x, y:e.y, vx:Math.cos(ang2)*5, vy:Math.sin(ang2)*5, life:150, dmg:12, c:'#fff'}); } | |
| spawnText('BOSS BLAST', e.x, e.y-30, '#fff'); | |
| } | |
| // Hit by Bullets | |
| for(let bi=bullets.length-1; bi>=0; bi--){ let b=bullets[bi]; if(Math.hypot(e.x-b.x, e.y-b.y) < e.r+5){ e.hp -= b.dmg; bullets.splice(bi,1); spawnText(Math.floor(b.dmg), e.x, e.y-10, '#fff'); for(let k=0; k<3; k++) particles.push({x:e.x, y:e.y, vx:(Math.random()-0.5)*5, vy:(Math.random()-0.5)*5, life:20, c:e.color, s:3}); }} | |
| if(e.hp <= 0){ | |
| enemies.splice(i,1); | |
| score += (e.type==='boss'?500:(e.type==='mini_boss'?120:10)); | |
| player.rage += (e.type==='boss'?10:2); | |
| spawnPickups(e.x, e.y); | |
| screenshake(6); | |
| if(player.stats.explosion){ enemies.forEach(n => { let d = Math.hypot(n.x-e.x, n.y-e.y); if(d < 80){ n.hp -= 25; particles.push({x:n.x,y:n.y,vx: (Math.random()-0.5)*4, vy:(Math.random()-0.5)*4, life:25, c:'#f80', s:3}); }}); } | |
| } | |
| if(Math.hypot(player.x-e.x, player.y-e.y) < player.r+e.r && player.invincible<=0){ | |
| player.hp -= 15; | |
| player.invincible = 40; | |
| screenshake(18); | |
| particles.push({x:player.x, y:player.y, vx:0, vy:0, life:30, c:'red', s:50}); | |
| if(player.hp<=0) die(); | |
| } | |
| } | |
| // Mining | |
| for(let i=ores.length-1;i>=0;i--){ let o=ores[i]; if(Math.hypot(player.x-o.x, player.y-o.y) < o.r+30){ if(frame%5===0){ o.hp-=2; player.matter++; particles.push({x:o.x, y:o.y, vx:rnd(-2,2), vy:rnd(-2,2), life:20, c:'gold', s:2}); if(o.hp<=0){ ores.splice(i,1); setTimeout(spawnOre, 15000); } } } } | |
| // Pickups | |
| for(let i=pickups.length-1;i>=0;i--){ let p=pickups[i]; let d = Math.hypot(player.x-p.x, player.y-p.y); if(d < player.stats.pickup){ p.x += (player.x-p.x)*0.15; p.y += (player.y-p.y)*0.15; } if(d < 10){ if(p.type==='xp') { player.xp += p.val; if(player.xp >= player.nextLvl) levelUp(); } if(p.type==='heal') player.hp = Math.min(player.maxHp, player.hp+20); pickups.splice(i,1); }} | |
| // Particle Cleanup | |
| for(let i=particles.length-1;i>=0;i--){ let p=particles[i]; p.x+=p.vx; p.y+=p.vy; p.life--; if(p.life<=0) particles.splice(i,1); } | |
| // Text Cleanup | |
| for(let i=texts.length-1;i>=0;i--){ let t=texts[i]; t.y-=1; t.life--; if(t.life<=0) texts.splice(i,1); } | |
| // Regen passive | |
| if(player.stats.regen && frame % 120 === 0){ player.hp = Math.min(player.maxHp, player.hp+1); } | |
| updateUI(); | |
| frame++; | |
| } | |
| function spawnPickups(x,y){ | |
| pickups.push({x,y,type:'xp',val:20, c:'#0f0'}); | |
| if(Math.random()>0.85) pickups.push({x:x+10,y:y,type:'heal',val:20, c:'red'}); | |
| } | |
| function spawnText(str, x, y, c){ texts.push({str, x, y, c, life:60}); } | |
| function die(){ | |
| gameState = 'OVER'; | |
| document.getElementById('death-screen').style.display = 'flex'; | |
| document.getElementById('final-score').innerText = "SCORE: " + score; | |
| } | |
| function updateUI(){ | |
| document.getElementById('hp-fill').style.width = Math.max(0, (player.hp/player.maxHp*100))+'%'; | |
| document.getElementById('xp-fill').style.width = (player.xp/player.nextLvl*100)+'%'; | |
| document.getElementById('rage-fill').style.width = (player.rage/player.maxRage*100)+'%'; | |
| document.getElementById('matter-display').innerText = player.matter; | |
| document.getElementById('game-time').innerText = Math.floor(timeSeconds/60) + ":" + (timeSeconds%60).toString().padStart(2,'0'); | |
| document.getElementById('wave-num').innerText = Math.floor(1 + timeSeconds/60); | |
| document.getElementById('hp-value').innerText = Math.max(0, Math.floor(player.hp)) + ' / ' + player.maxHp; | |
| document.getElementById('xp-value').innerText = Math.floor(player.xp) + ' / ' + player.nextLvl; | |
| document.getElementById('rage-value').innerText = Math.floor((player.rage/player.maxRage*100)) + '%'; | |
| document.getElementById('lvl-display').innerText = player.lvl; | |
| document.getElementById('score-display').innerText = score; | |
| document.getElementById('intensity-display').innerText = 'INTENSITY: ' + intensity.toFixed(2); | |
| let cdDash = document.getElementById('cd-dash'); | |
| cdDash.className = player.dash<=0 ? 'cd-ready' : 'cd-wait'; | |
| let cdSlow = document.getElementById('cd-slow'); | |
| cdSlow.className = player.slow<=0 ? 'cd-ready' : 'cd-wait'; | |
| } | |
| function draw(){ | |
| // Clear & Shake | |
| ctx.fillStyle = '#080808'; | |
| ctx.fillRect(0,0,W,H); | |
| ctx.save(); | |
| if(shake > 0) ctx.translate(rnd(-shake, shake), rnd(-shake, shake)); | |
| // Background Grid (pulse with intensity) | |
| ctx.strokeStyle = `rgba(30,30,30,${0.6 + Math.min(0.5, intensity*0.02)})`; | |
| ctx.lineWidth = 1; | |
| ctx.beginPath(); | |
| for(let i=0; i<W; i+=80) { ctx.moveTo(i,0); ctx.lineTo(i,H); } | |
| for(let i=0; i<H; i+=80) { ctx.moveTo(0,i); ctx.lineTo(W,i); } | |
| ctx.stroke(); | |
| // Aura | |
| if(player.auraUnlocked){ | |
| ctx.save(); | |
| ctx.translate(player.x, player.y); | |
| ctx.beginPath(); | |
| ctx.arc(0,0,player.stats.auraRange, 0, Math.PI*2); | |
| ctx.fillStyle = `rgba(255, 0, 255, ${0.08 + Math.sin(frame*0.1)*0.04})`; | |
| ctx.fill(); | |
| ctx.strokeStyle = '#f0f'; | |
| ctx.lineWidth = 2; | |
| ctx.stroke(); | |
| ctx.restore(); | |
| } | |
| // Ores | |
| ores.forEach(o => { | |
| ctx.shadowBlur = 14; ctx.shadowColor = 'gold'; | |
| ctx.fillStyle = '#ccaa00'; | |
| ctx.beginPath(); ctx.arc(o.x, o.y, o.r, 0, Math.PI*2); ctx.fill(); | |
| ctx.fillStyle = '#fff'; ctx.beginPath(); ctx.arc(o.x-5, o.y-5, 5, 0, Math.PI*2); ctx.fill(); | |
| }); | |
| // Turrets | |
| turrets.forEach(t => { | |
| ctx.shadowBlur = 10; ctx.shadowColor = 'lime'; | |
| ctx.fillStyle = '#222'; | |
| ctx.beginPath(); ctx.arc(t.x, t.y, 12, 0, Math.PI*2); ctx.fill(); | |
| ctx.strokeStyle = 'lime'; ctx.lineWidth=3; | |
| ctx.beginPath(); ctx.moveTo(t.x, t.y-5); ctx.lineTo(t.x, t.y+5); ctx.moveTo(t.x-5, t.y); ctx.lineTo(t.x+5, t.y); ctx.stroke(); | |
| }); | |
| // Enemies | |
| enemies.forEach(e => { | |
| ctx.shadowBlur = 10; ctx.shadowColor = e.color; | |
| ctx.fillStyle = e.color; | |
| ctx.save(); | |
| ctx.translate(e.x, e.y); | |
| ctx.rotate(Math.atan2(player.y-e.y, player.x-e.x)); | |
| if(e.type === 'boss'){ | |
| ctx.beginPath(); ctx.arc(0,0,e.r,0,Math.PI*2); ctx.fill(); | |
| ctx.fillStyle = 'black'; ctx.beginPath(); ctx.moveTo(10, -20); ctx.lineTo(30, -30); ctx.lineTo(30, -10); ctx.moveTo(10, 20); ctx.lineTo(30, 30); ctx.lineTo(30, 10); ctx.fill(); | |
| ctx.strokeStyle = '#fff'; ctx.lineWidth=2; ctx.beginPath(); ctx.moveTo(10, -10); for(let k=0;k<5;k++) ctx.lineTo(15+k*5, (k%2===0?5:-5)); ctx.stroke(); | |
| } else { | |
| if(e.type==='tank') ctx.fillRect(-e.r, -e.r, e.r*2, e.r*2); | |
| else if(e.type==='rusher') { ctx.beginPath(); ctx.moveTo(e.r,0); ctx.lineTo(-e.r, -e.r); ctx.lineTo(-e.r, e.r); ctx.fill(); } | |
| else if(e.type==='mini_boss') { ctx.beginPath(); ctx.arc(0,0,e.r,0,Math.PI*2); ctx.fill(); ctx.strokeStyle='#000'; ctx.lineWidth=3; ctx.stroke(); } | |
| else { ctx.beginPath(); ctx.arc(0,0,e.r,0,Math.PI*2); ctx.fill(); } | |
| ctx.fillStyle = 'rgba(0,0,0,0.6)'; ctx.beginPath(); ctx.arc(5, -5, 3, 0, Math.PI*2); ctx.fill(); ctx.beginPath(); ctx.arc(5, 5, 3, 0, Math.PI*2); ctx.fill(); | |
| } | |
| ctx.restore(); | |
| let hpPct = Math.max(0, e.hp/e.max); | |
| ctx.fillStyle = 'red'; ctx.fillRect(e.x-20, e.y-e.r-12, 40, 6); | |
| ctx.fillStyle = '#0f0'; ctx.fillRect(e.x-20, e.y-e.r-12, 40*hpPct, 6); | |
| // draw face (centered) | |
| ctx.font = (e.r/2) + 'px monospace'; | |
| ctx.textAlign = 'center'; | |
| ctx.textBaseline = 'middle'; | |
| ctx.fillStyle = '#fff'; | |
| let face = e.face || choose(FACE_VARIANTS); | |
| // make face change when low HP | |
| if(e.hp < e.max * 0.35) face = face + ' !'; | |
| ctx.fillText(face, e.x, e.y); | |
| }); | |
| // Bullets | |
| bullets.forEach(b => { ctx.shadowColor = b.c; ctx.shadowBlur = 10; ctx.fillStyle = b.c; ctx.beginPath(); ctx.arc(b.x, b.y, 5, 0, Math.PI*2); ctx.fill(); }); | |
| // Drones | |
| drones.forEach(d => { ctx.shadowColor = 'lime'; ctx.shadowBlur = 10; ctx.fillStyle = 'lime'; ctx.beginPath(); ctx.arc(d.x, d.y, 6, 0, Math.PI*2); ctx.fill(); ctx.strokeStyle = '#0f0'; ctx.lineWidth = 1; ctx.beginPath(); ctx.moveTo(player.x, player.y); ctx.lineTo(d.x, d.y); ctx.stroke(); }); | |
| // Pickups | |
| pickups.forEach(p => { ctx.shadowColor = p.c; ctx.shadowBlur = 5; ctx.fillStyle = p.c; ctx.beginPath(); ctx.arc(p.x, p.y, 4, 0, Math.PI*2); ctx.fill(); }); | |
| // Player Trails | |
| player.trails.forEach(t => { ctx.fillStyle = player.rageActive ? `rgba(255,165,0,${t.alpha})` : `rgba(0,255,255,${t.alpha})`; ctx.beginPath(); ctx.arc(t.x, t.y, player.r, 0, Math.PI*2); ctx.fill(); }); | |
| // Player | |
| ctx.shadowBlur = 20; ctx.shadowColor = player.rageActive ? 'orange' : 'cyan'; ctx.fillStyle = player.rageActive ? 'orange' : 'cyan'; | |
| if(player.invincible > 0 && frame%4===0) ctx.fillStyle = 'white'; | |
| ctx.save(); ctx.translate(player.x, player.y); ctx.rotate(Math.atan2(mouse.y-player.y, mouse.x-player.x)); ctx.fillRect(-12, -12, 24, 24); ctx.fillStyle = '#000'; ctx.fillRect(0, -5, 10, 10); ctx.restore(); | |
| // Particles | |
| particles.forEach(p => { ctx.fillStyle = p.c; ctx.globalAlpha = Math.max(0, p.life/30); ctx.beginPath(); ctx.arc(p.x, p.y, p.s, 0, Math.PI*2); ctx.fill(); ctx.globalAlpha = 1; }); | |
| // Text Popups | |
| texts.forEach(t => { ctx.fillStyle = t.c; ctx.font = "bold 18px monospace"; ctx.fillText(t.str, t.x, t.y); }); | |
| ctx.restore(); | |
| } | |
| function renderLoop(){ | |
| if(gameState === 'PLAY'){ | |
| update(); | |
| draw(); | |
| } | |
| loopId = requestAnimationFrame(renderLoop); | |
| } | |
| // Start | |
| resize(); | |
| </script> | |
| </body> | |
| </html> | |