blocky-builder-deluxe / index.html
aappeell's picture
Manual changes saved
35245bf verified
<!DOCTYPE html>
<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>