VOIDDASH / app.py
dwishank's picture
Update app.py
09d129e verified
import streamlit as st
import streamlit.components.v1 as components
st.set_page_config(page_title="Void Dash", page_icon="⚡", layout="wide")
st.markdown("""
<style>
body, .stApp { background:#05070f !important; }
#MainMenu, footer, header { visibility:hidden; }
.block-container { padding:0.5rem 1rem; }
</style>
""", unsafe_allow_html=True)
GAME_HTML = r"""
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<style>
@import url('https://fonts.googleapis.com/css2?family=Orbitron:wght@400;700;900&family=Share+Tech+Mono&display=swap');
*{margin:0;padding:0;box-sizing:border-box;}
body{background:#05070f;display:flex;flex-direction:column;align-items:center;font-family:'Share Tech Mono',monospace;overflow:hidden;}
canvas{display:block;border-radius:14px;cursor:pointer;}
#hud{
display:flex;justify-content:space-between;align-items:center;
width:820px;padding:5px 14px;font-size:12px;color:#aaa;
background:linear-gradient(90deg,#080a18,#0d0f22,#080a18);
border-radius:0 0 12px 12px;border-top:1px solid #1a1c35;
}
#lives-bar span{display:inline-block;width:11px;height:11px;border-radius:50%;margin:0 2px;transition:background .3s;}
#shield-bar{width:65px;height:5px;background:#111;border-radius:3px;overflow:hidden;display:inline-block;}
#shield-fill{height:100%;background:linear-gradient(90deg,#3ecfcf,#7ff0f0);width:0%;border-radius:3px;transition:width .2s;}
#char-hud{font-size:11px;color:#a99ff7;letter-spacing:1px;}
#pubar{display:flex;gap:5px;justify-content:center;width:820px;padding:3px 0;font-size:10px;}
.pu-slot{display:flex;align-items:center;gap:3px;padding:2px 8px;border-radius:7px;
background:#080a16;color:#333;transition:all .25s;border:1px solid transparent;}
.pu-slot.on{color:#fff;}
.pu-slot.on.boost {background:#081a0e;color:#3ecf8e;border-color:#1a4a2a;}
.pu-slot.on.freeze{background:#080e1a;color:#5bb8ff;border-color:#1a2a4a;}
.pu-slot.on.gun {background:#130820;color:#d46bff;border-color:#3a1a5a;}
.pu-slot.on.slow {background:#1a1400;color:#ffcc44;border-color:#4a3a00;}
.pu-slot.on.tiny {background:#001a0a;color:#44ffaa;border-color:#005a2a;}
.pu-slot.on.magnet{background:#08001a;color:#aa88ff;border-color:#2a005a;}
#combo-row{width:820px;display:flex;align-items:center;gap:8px;padding:2px 14px;}
#combo-label{font-size:10px;color:#555;min-width:70px;}
#combo-track{flex:1;height:4px;background:#0d0f22;border-radius:2px;overflow:hidden;}
#combo-fill{height:100%;width:0%;background:linear-gradient(90deg,#f7c948,#f76464);border-radius:2px;transition:width .1s;}
#combo-mult{font-size:11px;color:#f7c948;min-width:30px;text-align:right;}
#char-select{
position:fixed;inset:0;background:#03040c;z-index:100;
display:flex;flex-direction:column;align-items:center;justify-content:center;gap:18px;
}
#char-select h1{font-family:'Orbitron',monospace;font-size:28px;color:#a99ff7;letter-spacing:6px;
text-shadow:0 0 30px rgba(169,159,247,0.5);}
#char-select p{color:#445;font-size:12px;letter-spacing:2px;}
#char-cards{display:flex;gap:14px;}
.char-card{
width:132px;border:1px solid #1e1f3a;border-radius:14px;padding:14px 10px;
background:#07080f;cursor:pointer;transition:all .2s;text-align:center;
display:flex;flex-direction:column;align-items:center;gap:8px;
}
.char-card:hover{border-color:#a99ff7;background:#0d0e1e;transform:translateY(-4px);box-shadow:0 8px 24px rgba(100,80,200,0.2);}
.char-card.selected{border-color:#a99ff7;background:#100f2a;box-shadow:0 0 20px rgba(169,159,247,0.25);}
.char-canvas{border-radius:50%;background:#0a0b18;border:2px solid #1e1f3a;}
.char-name{font-family:'Orbitron',monospace;font-size:10px;color:#a99ff7;letter-spacing:2px;}
.char-role{font-size:9px;color:#446;margin-top:-4px;}
.char-stats{width:100%;display:flex;flex-direction:column;gap:3px;}
.stat-row{display:flex;justify-content:space-between;align-items:center;font-size:9px;}
.stat-label{color:#445;}
.stat-bar{width:55px;height:3px;background:#0d0e1e;border-radius:2px;overflow:hidden;}
.stat-fill{height:100%;border-radius:2px;}
.char-perk{font-size:9px;color:#7c6af7;line-height:1.5;padding:6px 4px;
background:#0a0820;border-radius:6px;border:1px solid #1a1545;}
#start-btn{
font-family:'Orbitron',monospace;font-size:13px;letter-spacing:3px;
padding:10px 40px;border-radius:8px;border:1px solid #a99ff7;
background:transparent;color:#a99ff7;cursor:pointer;transition:all .2s;
}
#start-btn:hover{background:#a99ff7;color:#05070f;}
#wrap{position:relative;}
#msg{
position:absolute;top:50%;left:50%;transform:translate(-50%,-52%);
color:#fff;text-align:center;pointer-events:none;min-width:290px;
font-family:'Share Tech Mono',monospace;z-index:10;
}
#msg h2{font-family:'Orbitron',monospace;font-size:20px;letter-spacing:4px;margin:0 0 8px;}
#msg p{font-size:11px;color:#778;margin:2px 0;line-height:1.7;}
#lb{
position:absolute;top:8px;right:8px;width:138px;
background:rgba(6,7,18,0.95);border-radius:10px;border:1px solid #141530;
padding:8px 7px;z-index:5;
}
#lb h3{font-family:'Orbitron',monospace;color:#a99ff7;font-size:9px;text-align:center;
margin-bottom:6px;letter-spacing:2px;}
.lb-row{display:flex;justify-content:space-between;padding:2px 4px;
border-radius:4px;margin:1px 0;font-size:10px;}
.g{background:#1f1500;color:#f7c948;}
.s{background:#141420;color:#b0b8d0;}
.b{background:#160e04;color:#cd7f32;}
.n{background:#08091a;color:#444;}
.hl{border:1px solid #7c6af7;color:#a99ff7!important;background:#100f2a!important;}
.lb-empty{color:#222;font-size:9px;text-align:center;padding:8px 0;}
.tag{display:inline-block;font-size:10px;padding:2px 7px;border-radius:8px;margin:2px;}
</style>
</head>
<body>
<!-- ══ CHARACTER SELECT ══════════════════════════════════════════════════════ -->
<div id="char-select">
<h1>⚡ VOID DASH</h1>
<p>CHOOSE YOUR PILOT</p>
<div id="char-cards"></div>
<button id="start-btn" onclick="startGame()">LAUNCH →</button>
</div>
<!-- ══ GAME ══════════════════════════════════════════════════════════════════ -->
<div id="wrap" style="display:none;">
<canvas id="gc" width="820" height="420"></canvas>
<div id="msg" style="display:none;"></div>
<div id="lb"><h3>🏆 TOP 10</h3><div id="lb-list"><div class="lb-empty">No scores</div></div></div>
</div>
<div id="hud" style="display:none;">
<span id="char-hud">PILOT</span>
<span>Score:<b id="sc" style="color:#fff"> 0</b> &nbsp; Best:<b id="hi" style="color:#a99ff7"> 0</b></span>
<span>♥ <span id="lives-bar"></span>
&nbsp;🛡<span id="shield-bar"><span id="shield-fill"></span></span>
</span>
</div>
<div id="combo-row" style="display:none;">
<span id="combo-label">COMBO x1</span>
<div id="combo-track"><div id="combo-fill"></div></div>
<span id="combo-mult"></span>
</div>
<div id="pubar" style="display:none;">
<div class="pu-slot boost" id="sl-boost" >⚡<span id="tx-boost" ></span></div>
<div class="pu-slot freeze" id="sl-freeze">❄<span id="tx-freeze"></span></div>
<div class="pu-slot gun" id="sl-gun" >☆<span id="tx-gun" ></span></div>
<div class="pu-slot slow" id="sl-slow" >⏳<span id="tx-slow" ></span></div>
<div class="pu-slot tiny" id="sl-tiny" >🔬<span id="tx-tiny" ></span></div>
<div class="pu-slot magnet" id="sl-magnet">🧲<span id="tx-magnet"></span></div>
</div>
<script>
// ══════════════════════════════════════════════════════════════════════════════
// CHARACTERS
// ══════════════════════════════════════════════════════════════════════════════
const CHARACTERS=[
{
id:'nova',name:'NOVA',role:'Speedster',
color:'#a99ff7',glow:'rgba(169,159,247,0.4)',r:10,
stats:{speed:5,lives:3,shield:3,luck:3},
perk:'Balanced starter. Speed increases faster each run.',
passive:'speed_boost',
draw(ctx,x,y,t,r){
const g=ctx.createRadialGradient(x,y,1,x,y,r*1.4);
g.addColorStop(0,'#fff');g.addColorStop(0.4,'#c8beff');g.addColorStop(1,'rgba(169,159,247,0)');
ctx.beginPath();ctx.arc(x,y,r*1.4,0,Math.PI*2);ctx.fillStyle=g;ctx.fill();
ctx.beginPath();ctx.arc(x,y,r,0,Math.PI*2);ctx.fillStyle='#a99ff7';ctx.fill();
ctx.beginPath();ctx.arc(x-r*0.3,y-r*0.3,r*0.28,0,Math.PI*2);ctx.fillStyle='rgba(255,255,255,0.55)';ctx.fill();
}
},
{
id:'blaze',name:'BLAZE',role:'Brawler',
color:'#ff7744',glow:'rgba(255,100,50,0.4)',r:11,
stats:{speed:3,lives:5,shield:2,luck:2},
perk:'+2 extra lives. Stomping enemies bounces higher & gives bonus points.',
passive:'extra_lives',
draw(ctx,x,y,t,r){
ctx.save();ctx.translate(x,y);ctx.rotate(t*0.04);
for(let i=0;i<6;i++){
ctx.save();ctx.rotate(i*Math.PI/3);
ctx.beginPath();ctx.moveTo(0,-r*1.3);ctx.lineTo(r*0.4,-r*0.5);ctx.lineTo(0,-r*0.25);ctx.lineTo(-r*0.4,-r*0.5);ctx.closePath();
ctx.fillStyle=i%2===0?'#ff7744':'#ffaa44';ctx.fill();
ctx.restore();
}
ctx.beginPath();ctx.arc(0,0,r*0.55,0,Math.PI*2);ctx.fillStyle='#ffe0c0';ctx.fill();
ctx.restore();
}
},
{
id:'ghost',name:'GHOST',role:'Phantom',
color:'#44ffcc',glow:'rgba(68,255,200,0.35)',r:9,
stats:{speed:4,lives:3,shield:5,luck:3},
perk:'Starts with full shield. Shield slowly regenerates over time.',
passive:'shield_regen',
draw(ctx,x,y,t,r){
ctx.save();ctx.translate(x,y);
ctx.globalAlpha=0.82+0.12*Math.sin(t*0.08);
ctx.beginPath();
ctx.arc(0,0,r,0,Math.PI);
ctx.lineTo(-r,r*0.55);ctx.lineTo(-r*0.5,r*0.05);ctx.lineTo(0,r*0.5);ctx.lineTo(r*0.5,r*0.05);ctx.lineTo(r,r*0.55);
ctx.closePath();ctx.fillStyle='#44ffcc';ctx.fill();
ctx.globalAlpha=1;ctx.fillStyle='#03040c';
ctx.beginPath();ctx.arc(-r*0.32,-r*0.1,r*0.17,0,Math.PI*2);ctx.fill();
ctx.beginPath();ctx.arc(r*0.32,-r*0.1,r*0.17,0,Math.PI*2);ctx.fill();
ctx.restore();
}
},
{
id:'storm',name:'STORM',role:'Gunner',
color:'#ffdd44',glow:'rgba(255,220,68,0.4)',r:10,
stats:{speed:3,lives:3,shield:2,luck:5},
perk:'Gun lasts 10s instead of 5s. Lucky power-up drops. Shoots faster.',
passive:'gun_duration',
draw(ctx,x,y,t,r){
ctx.save();ctx.translate(x,y);ctx.rotate(Math.sin(t*0.05)*0.12);
ctx.beginPath();
ctx.moveTo(r*0.3,-r*1.2);ctx.lineTo(-r*0.2,-r*0.05);ctx.lineTo(r*0.3,-r*0.05);
ctx.lineTo(-r*0.3,r*1.2);ctx.lineTo(r*0.2,r*0.05);ctx.lineTo(-r*0.3,r*0.05);
ctx.closePath();ctx.fillStyle='#ffdd44';ctx.fill();
ctx.strokeStyle='rgba(255,255,200,0.5)';ctx.lineWidth=1;ctx.stroke();
ctx.restore();
}
},
{
id:'void',name:'V0ID',role:'Wraith',
color:'#cc44ff',glow:'rgba(200,68,255,0.4)',r:10,
stats:{speed:5,lives:2,shield:4,luck:4},
perk:'Tiny lasts 10s. Every 3rd hit is phased (ignored). High risk / reward.',
passive:'phase',
draw(ctx,x,y,t,r){
ctx.save();ctx.translate(x,y);
for(let i=0;i<3;i++){
ctx.save();ctx.rotate(t*0.07+i*Math.PI*2/3);
ctx.beginPath();ctx.ellipse(r*0.55,0,r*0.62,r*0.22,0,0,Math.PI*2);
ctx.fillStyle=`hsla(${280+i*22},80%,58%,0.72)`;ctx.fill();
ctx.restore();
}
ctx.beginPath();ctx.arc(0,0,r*0.5,0,Math.PI*2);ctx.fillStyle='#cc44ff';ctx.fill();
ctx.beginPath();ctx.arc(0,0,r*0.2,0,Math.PI*2);ctx.fillStyle='#fff';ctx.fill();
ctx.restore();
}
}
];
// ══════════════════════════════════════════════════════════════════════════════
// LEADERBOARD
// ══════════════════════════════════════════════════════════════════════════════
const LS='voidDash_v5';
function loadScores(){try{return JSON.parse(localStorage.getItem(LS))||[];}catch(e){return[];}}
function saveScores(a){try{localStorage.setItem(LS,JSON.stringify(a));}catch(e){}}
function addScore(s,charId){
const a=loadScores();a.push({s,charId});a.sort((x,y)=>y.s-x.s);
const t=a.slice(0,10);saveScores(t);return t;
}
function renderLB(hl){
const a=loadScores(),el=document.getElementById('lb-list');
if(!a.length){el.innerHTML='<div class="lb-empty">Play first!</div>';return;}
const medals=['🥇','🥈','🥉'];
el.innerHTML=a.map(({s,charId},i)=>{
let cls=i===0?'g':i===1?'s':i===2?'b':'n';
if(hl!==undefined&&s===hl)cls+=' hl';
const ch=CHARACTERS.find(c=>c.id===charId)||CHARACTERS[0];
return `<div class="lb-row ${cls}"><span>${i<3?medals[i]:(i+1)+'.'}</span><span style="color:${ch.color};font-size:9px">${ch.name}</span><span>${s}</span></div>`;
}).join('');
}
// ══════════════════════════════════════════════════════════════════════════════
// CHARACTER SELECT SCREEN
// ══════════════════════════════════════════════════════════════════════════════
let selectedChar=CHARACTERS[0];
function buildCharSelect(){
const container=document.getElementById('char-cards');
CHARACTERS.forEach((ch,idx)=>{
const card=document.createElement('div');
card.className='char-card'+(idx===0?' selected':'');
card.id='card-'+ch.id;
const cv=document.createElement('canvas');
cv.width=68;cv.height=68;cv.className='char-canvas';
animCharPreview(cv,ch);
card.appendChild(cv);
const statNames={speed:'SPD',lives:'HP',shield:'SHD',luck:'LCK'};
const statColors={speed:'#a99ff7',lives:'#f76464',shield:'#3ecfcf',luck:'#ffdd44'};
const nameDiv=document.createElement('div');nameDiv.className='char-name';nameDiv.textContent=ch.name;
const roleDiv=document.createElement('div');roleDiv.className='char-role';roleDiv.textContent=ch.role;
card.appendChild(nameDiv);card.appendChild(roleDiv);
const statsDiv=document.createElement('div');statsDiv.className='char-stats';
Object.entries(ch.stats).forEach(([k,v])=>{
statsDiv.innerHTML+=`<div class="stat-row"><span class="stat-label">${statNames[k]}</span><div class="stat-bar"><div class="stat-fill" style="width:${v/5*100}%;background:${statColors[k]}"></div></div></div>`;
});
card.appendChild(statsDiv);
const perkDiv=document.createElement('div');perkDiv.className='char-perk';perkDiv.textContent=ch.perk;
card.appendChild(perkDiv);
card.onclick=()=>{
document.querySelectorAll('.char-card').forEach(c=>c.classList.remove('selected'));
card.classList.add('selected');
selectedChar=ch;
};
container.appendChild(card);
});
}
function animCharPreview(cv,ch){
const ctx2=cv.getContext('2d');
let t=0;
(function draw(){
ctx2.clearRect(0,0,68,68);
ctx2.fillStyle='#07080f';ctx2.fillRect(0,0,68,68);
const g=ctx2.createRadialGradient(34,34,2,34,34,28);
g.addColorStop(0,ch.glow);g.addColorStop(1,'transparent');
ctx2.beginPath();ctx2.arc(34,34,28,0,Math.PI*2);ctx2.fillStyle=g;ctx2.fill();
ch.draw(ctx2,34,34,t,ch.r*1.5);
t++;requestAnimationFrame(draw);
})();
}
buildCharSelect();
function startGame(){
document.getElementById('char-select').style.display='none';
document.getElementById('wrap').style.display='block';
document.getElementById('hud').style.display='flex';
document.getElementById('combo-row').style.display='flex';
document.getElementById('pubar').style.display='flex';
document.getElementById('char-hud').textContent=selectedChar.name+' // '+selectedChar.role;
document.getElementById('char-hud').style.color=selectedChar.color;
initLivesUI();
renderLB();
showIdleMsg();
if(!loopRunning){loopRunning=true;loop();}
}
// ══════════════════════════════════════════════════════════════════════════════
// GAME ENGINE
// ══════════════════════════════════════════════════════════════════════════════
const C=document.getElementById('gc'),ctx=C.getContext('2d');
const W=C.width,H=C.height;
const GRAV=0.38,JUMP=-7.5,GLIDE=0.15;
const PIPE_W=46,GAP=130,BASE_SPD=2.9,PIPE_INT=190;
const MAX_LIVES=3;
const DUR={boost:300,freeze:300,gun:300,slow:240,tiny:360,magnet:300};
let gframe=0,score=0,best=0,held=false,state='idle',loopRunning=false;
let bird,pipes,enemies,bullets,pBullets,particles,powerups,floatTexts;
let lives,shield,invT,stompFlash;
let timers={boost:0,freeze:0,gun:0,slow:0,tiny:0,magnet:0};
let combo=0,comboTimer=0,comboMult=1;
let shieldRegenTimer=0,phaseHits=0;
// ── HUD ──────────────────────────────────────────────────────────────────────
function initLivesUI(){
const maxL=MAX_LIVES+(selectedChar.passive==='extra_lives'?2:0);
const b=document.getElementById('lives-bar');b.innerHTML='';
for(let i=0;i<maxL;i++){
const s=document.createElement('span');s.id='lf'+i;
s.style.background=i<lives?selectedChar.color:'#1a1a2a';
b.appendChild(s);
}
}
function updLives(){
const maxL=MAX_LIVES+(selectedChar.passive==='extra_lives'?2:0);
for(let i=0;i<maxL;i++){const s=document.getElementById('lf'+i);if(s)s.style.background=i<lives?selectedChar.color:'#1a1a2a';}
}
function updShield(){document.getElementById('shield-fill').style.width=Math.round(shield)+'%';}
function updSlots(){
['boost','freeze','gun','slow','tiny','magnet'].forEach(k=>{
const sl=document.getElementById('sl-'+k),tx=document.getElementById('tx-'+k);
if(!sl)return;
if(timers[k]>0){sl.classList.add('on');tx.textContent=' '+Math.ceil(timers[k]/60)+'s';}
else{sl.classList.remove('on');tx.textContent='';}
});
}
function updCombo(){
const pct=Math.min(comboTimer/180*100,100);
document.getElementById('combo-fill').style.width=pct+'%';
document.getElementById('combo-label').textContent='COMBO x'+comboMult;
document.getElementById('combo-mult').textContent=combo>1?'('+combo+' chain)':'';
}
// ── Init ─────────────────────────────────────────────────────────────────────
function initGame(){
const maxL=MAX_LIVES+(selectedChar.passive==='extra_lives'?2:0);
lives=maxL;
shield=selectedChar.passive==='shield_regen'?100:0;
bird={x:120,y:H/2,vy:0,r:selectedChar.r,trail:[],char:selectedChar};
pipes=[];enemies=[];bullets=[];pBullets=[];particles=[];powerups=[];floatTexts=[];
gframe=0;score=0;invT=0;stompFlash=0;
timers={boost:0,freeze:0,gun:0,slow:0,tiny:0,magnet:0};
combo=0;comboTimer=0;comboMult=1;shieldRegenTimer=0;phaseHits=0;
document.getElementById('sc').textContent=' 0';
updLives();updShield();updSlots();updCombo();
}
// ── Input ────────────────────────────────────────────────────────────────────
function onJump(){
if(state==='idle'){state='play';document.getElementById('msg').style.display='none';initGame();return;}
if(state==='dead'){showCharSelect();return;}
if(state==='play')bird.vy=JUMP;
held=true;
}
function onRelease(){held=false;}
C.addEventListener('mousedown',onJump);
C.addEventListener('mouseup',onRelease);
C.addEventListener('touchstart',e=>{e.preventDefault();onJump();},{passive:false});
C.addEventListener('touchend',e=>{e.preventDefault();onRelease();},{passive:false});
window.addEventListener('keydown',e=>{if(e.code==='Space'){e.preventDefault();onJump();}});
window.addEventListener('keyup',e=>{if(e.code==='Space')onRelease();});
function showCharSelect(){
state='idle';
document.getElementById('char-select').style.display='flex';
document.getElementById('wrap').style.display='none';
document.getElementById('hud').style.display='none';
document.getElementById('combo-row').style.display='none';
document.getElementById('pubar').style.display='none';
}
function showIdleMsg(){
const msg=document.getElementById('msg');
msg.style.display='';
msg.innerHTML=`
<h2 style="color:${selectedChar.color}">${selectedChar.name}</h2>
<p style="color:#446;font-size:10px;letter-spacing:1px;margin-bottom:5px">${selectedChar.role.toUpperCase()} · ${selectedChar.perk}</p>
<p>
<span class="tag" style="background:#081a0e;color:#3ecf8e">⚡BOOST</span>
<span class="tag" style="background:#080e1a;color:#5bb8ff">❄FREEZE</span>
<span class="tag" style="background:#130820;color:#d46bff">☆GUN</span>
<span class="tag" style="background:#1a1400;color:#ffcc44">⏳SLOW</span>
</p>
<p>
<span class="tag" style="background:#001a0a;color:#44ffaa">🔬TINY</span>
<span class="tag" style="background:#08001a;color:#aa88ff">🧲MAGNET</span>
<span class="tag" style="background:#1a0008;color:#ff4466">💣NUKE</span>
<span class="tag" style="background:#2a2000;color:#ffe066">❤LIFE</span>
</p>
<p style="margin-top:5px;color:#444;font-size:10px">★ Fall ONTO aliens to STOMP &nbsp;|&nbsp; Chain kills = COMBO bonus</p>
<p style="margin-top:7px;color:${selectedChar.color};font-size:12px;letter-spacing:2px">TAP TO START</p>`;
}
// ── Helpers ──────────────────────────────────────────────────────────────────
function rnd(a,b){return a+Math.random()*(b-a);}
function hsl(h,s,l){return`hsl(${h},${s}%,${l}%)`;}
function circ(ax,ay,ar,bx,by,br){return Math.hypot(ax-bx,ay-by)<ar+br;}
function colPipe(b,p){return b.x+b.r>p.x&&b.x-b.r<p.x+PIPE_W&&(b.y-b.r<p.top||b.y+b.r>p.top+GAP);}
function gameSpd(){
let s=BASE_SPD+score*0.014;
if(selectedChar.passive==='speed_boost')s+=score*0.004;
if(timers.boost>0)s*=1.75;
if(timers.slow>0)s*=0.38;
return Math.min(s,8);
}
function effectiveR(){return timers.tiny>0?bird.r*0.48:bird.r;}
// ── Spawners ─────────────────────────────────────────────────────────────────
function spawnPipe(){pipes.push({x:W+10,top:55+Math.random()*(H-GAP-100),scored:false});}
function spawnEnemy(){
const hp=score>30?3:2;
enemies.push({x:W+20,y:rnd(35,H-35),vy:rnd(-0.7,0.7),r:14,shootT:rnd(40,110),hp,maxHp:hp,f:0,hue:rnd(0,360)});
}
function spawnPU(){
const luck=selectedChar.stats.luck||3;
const weights=[
{type:'boost',w:18},{type:'heart',w:14},{type:'freeze',w:14},
{type:'gun',w:13},{type:'slow',w:13},{type:'tiny',w:12},
{type:'magnet',w:10},{type:'nuke',w:2+Math.max(0,(luck-3)*1.5)}
];
if(selectedChar.passive==='gun_duration')weights.find(x=>x.type==='gun').w+=14;
const total=weights.reduce((s,x)=>s+x.w,0);
let roll=Math.random()*total,type='boost';
for(const {type:t,w} of weights){roll-=w;if(roll<=0){type=t;break;}}
powerups.push({x:W+10,y:rnd(55,H-55),type,r:13,bob:Math.random()*Math.PI*2});
}
function burst(x,y,col,n=8,spd=3.5){
for(let i=0;i<n;i++){
const a=Math.random()*Math.PI*2,s=rnd(1,spd);
particles.push({x,y,vx:Math.cos(a)*s,vy:Math.sin(a)*s,life:1,col,r:rnd(1.5,4)});
}
}
function floatText(x,y,txt,col='#fff',size=14){
floatTexts.push({x,y,txt,col,size,life:1,vy:-1.3});
}
// ── Score / combo ─────────────────────────────────────────────────────────────
function addPoints(n,x,y,label){
const earned=Math.round(n*comboMult);
score+=earned;
if(score>best)best=score;
document.getElementById('sc').textContent=' '+score;
document.getElementById('hi').textContent=' '+best;
combo++;comboTimer=190;
comboMult=combo>=5?4:combo>=3?3:combo>=2?2:1;
updCombo();
const lbl=label||('+'+earned+(comboMult>1?' ×'+comboMult:''));
floatText(x,y,lbl,comboMult>=4?'#f7c948':comboMult>=3?'#ffaa44':comboMult>=2?'#ffdd88':'#fff',comboMult>1?16:12);
}
// ── Damage / death ────────────────────────────────────────────────────────────
function takeDamage(x,y){
if(invT>0)return;
if(selectedChar.passive==='phase'){
phaseHits=(phaseHits+1)%3;
if(phaseHits===0){floatText(bird.x,bird.y-22,'PHASE!','#cc44ff',14);burst(bird.x,bird.y,'#cc44ff',8);return;}
}
if(shield>=25){shield=Math.max(0,shield-25);updShield();burst(bird.x,bird.y,'#3ecfcf',6);floatText(bird.x,bird.y-18,'SHIELD','#3ecfcf',11);return;}
lives--;updLives();invT=100;burst(x,y,'#f76464',12);
if(lives<=0)die();
}
function die(){
state='dead';
addScore(score,selectedChar.id);renderLB(score);
const msg=document.getElementById('msg');msg.style.display='';
msg.innerHTML=`<h2 style="color:#f76464;font-size:18px">DESTROYED</h2>
<p>Score: <b style="color:#fff">${score}</b> &nbsp; Best: <b style="color:${selectedChar.color}">${best}</b></p>
<p style="margin-top:4px;color:#a99ff7;font-size:10px">Score saved to leaderboard →</p>
<p style="margin-top:8px;color:#555;font-size:11px;letter-spacing:1px">TAP TO CHANGE PILOT</p>`;
}
// ── Draw background ───────────────────────────────────────────────────────────
function drawBg(t){
ctx.fillStyle='#05070f';ctx.fillRect(0,0,W,H);
// Nebula tint for active effects
if(timers.freeze>0){
const ng=ctx.createRadialGradient(W*.3,H*.5,50,W*.3,H*.5,320);
ng.addColorStop(0,'rgba(20,50,120,0.09)');ng.addColorStop(1,'transparent');
ctx.fillStyle=ng;ctx.fillRect(0,0,W,H);
}
if(timers.slow>0){
const sg=ctx.createRadialGradient(W*.7,H*.4,30,W*.7,H*.4,300);
sg.addColorStop(0,'rgba(90,70,0,0.1)');sg.addColorStop(1,'transparent');
ctx.fillStyle=sg;ctx.fillRect(0,0,W,H);
}
// Grid
ctx.strokeStyle=`rgba(80,60,200,${timers.slow>0?0.12:0.05})`;ctx.lineWidth=0.5;
const off=(t*gameSpd()*0.3)%42;
for(let x=-off;x<W;x+=42){ctx.beginPath();ctx.moveTo(x,0);ctx.lineTo(x,H);ctx.stroke();}
for(let y=0;y<H;y+=42){ctx.beginPath();ctx.moveTo(0,y);ctx.lineTo(W,y);ctx.stroke();}
// Stars — parallax layers
for(let i=0;i<80;i++){
const layer=i%3+1;
const bri=0.08+0.22*Math.sin(t*0.025+i*1.9);
ctx.fillStyle=`rgba(255,255,255,${bri})`;
ctx.fillRect((i*127+t*(0.04*layer))%W,(i*91+layer*30)%H,layer===3?2:1,layer===3?2:1);
}
}
// ── Draw pipe ─────────────────────────────────────────────────────────────────
function drawPipe(p,t){
const hue=(t*0.5+score*1.8)%360;
const g=ctx.createLinearGradient(p.x,0,p.x+PIPE_W,0);
g.addColorStop(0,hsl(hue,50,18));g.addColorStop(1,hsl(hue+45,55,32));
ctx.fillStyle=g;
ctx.beginPath();ctx.roundRect(p.x,0,PIPE_W,p.top,[0,0,8,8]);ctx.fill();
ctx.beginPath();ctx.roundRect(p.x,p.top+GAP,PIPE_W,H-p.top-GAP,[8,8,0,0]);ctx.fill();
ctx.strokeStyle=hsl(hue+60,88,58);ctx.lineWidth=1;ctx.stroke();
ctx.strokeStyle=`hsla(${hue+60},88%,68%,0.22)`;ctx.lineWidth=5;ctx.stroke();
}
// ── Draw bird ─────────────────────────────────────────────────────────────────
function drawBird(b,t){
const er=effectiveR();
// Trail
for(let i=0;i<b.trail.length;i++){
const pt=b.trail[i],a=(i/b.trail.length)*0.3;
ctx.beginPath();ctx.arc(pt.x,pt.y,er*(i/b.trail.length)*0.65,0,Math.PI*2);
ctx.fillStyle=`hsla(${(t*2+i*10)%360},90%,65%,${a})`;ctx.fill();
}
// Auras
if(timers.magnet>0){
ctx.beginPath();ctx.arc(b.x,b.y,85,0,Math.PI*2);
ctx.strokeStyle=`rgba(170,136,255,${0.12+0.06*Math.sin(t*0.18)})`;ctx.lineWidth=2;ctx.stroke();
ctx.strokeStyle=`rgba(170,136,255,0.05)`;ctx.lineWidth=6;ctx.stroke();
}
if(timers.boost>0){
ctx.beginPath();ctx.arc(b.x,b.y,er*2.8,0,Math.PI*2);
ctx.fillStyle=`rgba(62,207,207,${0.1+0.06*Math.sin(t*0.3)})`;ctx.fill();
}
if(timers.gun>0){
ctx.beginPath();ctx.arc(b.x,b.y,er*3,0,Math.PI*2);
ctx.fillStyle=`rgba(212,107,255,${0.1+0.06*Math.sin(t*0.4)})`;ctx.fill();
// Gun barrel
ctx.save();ctx.translate(b.x+er,b.y);
ctx.fillStyle='#d46bff';ctx.fillRect(0,-3.5,22,7);
ctx.fillStyle='#eeaaff';ctx.fillRect(20,-2.5,8,5);
ctx.restore();
}
if(timers.slow>0){
ctx.beginPath();ctx.arc(b.x,b.y,er*2.4,0,Math.PI*2);
ctx.fillStyle=`rgba(255,204,68,${0.1+0.05*Math.sin(t*0.25)})`;ctx.fill();
}
if(timers.tiny>0){
ctx.beginPath();ctx.arc(b.x,b.y,er*1.5,0,Math.PI*2);
ctx.strokeStyle=`rgba(68,255,170,${0.5+0.3*Math.sin(t*0.3)})`;ctx.lineWidth=1.5;ctx.stroke();
}
if(invT>0&&Math.floor(invT/4)%2===0)return;
b.char.draw(ctx,b.x,b.y,t,er);
}
// ── Draw enemy ────────────────────────────────────────────────────────────────
function drawEnemy(e,t){
e.f++;
const bob=Math.sin(e.f*0.05)*3;
const frozen=timers.freeze>0,slow=timers.slow>0;
ctx.save();ctx.translate(e.x,e.y+bob);
// Body
ctx.beginPath();ctx.ellipse(0,0,e.r*1.35,e.r,0,0,Math.PI*2);
ctx.fillStyle=frozen?'#0e1520':hsl(e.hue,78,32);ctx.fill();
ctx.strokeStyle=frozen?'#4499cc':slow?'#ccaa22':hsl((e.hue+t)%360,90,62);
ctx.lineWidth=1.5;ctx.stroke();
// Ice crystals
if(frozen){
ctx.strokeStyle='rgba(80,160,255,0.5)';ctx.lineWidth=1;
for(let a=0;a<6;a++){ctx.save();ctx.rotate(a*Math.PI/3);ctx.beginPath();ctx.moveTo(0,0);ctx.lineTo(0,e.r*1.05);ctx.stroke();ctx.restore();}
} else {
// Eyes
ctx.fillStyle='#fff';
ctx.beginPath();ctx.arc(-e.r*.38,-e.r*.1,e.r*.27,0,Math.PI*2);ctx.fill();
ctx.beginPath();ctx.arc(e.r*.38,-e.r*.1,e.r*.27,0,Math.PI*2);ctx.fill();
ctx.fillStyle='#111';
ctx.beginPath();ctx.arc(-e.r*.32,-e.r*.08,e.r*.13,0,Math.PI*2);ctx.fill();
ctx.beginPath();ctx.arc(e.r*.44,-e.r*.08,e.r*.13,0,Math.PI*2);ctx.fill();
// Antennae
ctx.strokeStyle='#f76464';ctx.lineWidth=1;
ctx.beginPath();ctx.moveTo(-e.r*.5,-e.r);ctx.lineTo(-e.r*.7,-e.r*1.6);ctx.stroke();
ctx.beginPath();ctx.arc(-e.r*.7,-e.r*1.65,2.5,0,Math.PI*2);ctx.fillStyle='#f76464';ctx.fill();
ctx.beginPath();ctx.moveTo(e.r*.5,-e.r);ctx.lineTo(e.r*.7,-e.r*1.6);ctx.stroke();
ctx.beginPath();ctx.arc(e.r*.7,-e.r*1.65,2.5,0,Math.PI*2);ctx.fill();
// Mouth (angrier at high score)
if(score>20){ctx.strokeStyle='#f76464';ctx.lineWidth=1.2;ctx.beginPath();ctx.arc(0,e.r*.3,e.r*.4,-Math.PI*0.1,Math.PI*1.1);ctx.stroke();}
}
ctx.restore();
// HP bar
const bw=12,bh=4,bx=e.x-((e.maxHp*bw+(e.maxHp-1)*2)/2);
for(let h=0;h<e.maxHp;h++){
ctx.fillStyle=h<e.hp?(frozen?'#5bb8ff':'#f76464'):'#1a1a2a';
ctx.fillRect(bx+h*(bw+2),e.y+bob-e.r-13,bw,bh);
}
}
// ── Draw power-up ─────────────────────────────────────────────────────────────
function drawPU(p,t){
p.bob+=0.065;
const py=p.y+Math.sin(p.bob)*6;
// Magnet pull
if(timers.magnet>0){
const dx=bird.x-p.x,dy=bird.y-p.y;
const d=Math.hypot(dx,dy);
if(d<150){p.x+=dx*0.06;p.y+=dy*0.06;}
}
ctx.save();ctx.translate(p.x,py);
const cfgs={
boost: {shape:'tri', col:'#3ecfcf',label:'B'},
heart: {shape:'circ', col:'#f76464',label:'+'},
freeze:{shape:'hex', col:'#5bb8ff',label:'❄'},
gun: {shape:'star5',col:'#d46bff',label:'☆'},
slow: {shape:'circ', col:'#ffcc44',label:'⏳'},
tiny: {shape:'hex', col:'#44ffaa',label:'◎'},
magnet:{shape:'tri', col:'#aa88ff',label:'M'},
nuke: {shape:'star5',col:'#ff4466',label:'!'},
};
const cfg=cfgs[p.type]||cfgs.boost;
const r=p.r;
// Outer glow
const g=ctx.createRadialGradient(0,0,1,0,0,r*2.2);
g.addColorStop(0,cfg.col+'55');g.addColorStop(1,'transparent');
ctx.beginPath();ctx.arc(0,0,r*2.2,0,Math.PI*2);ctx.fillStyle=g;ctx.fill();
// Pulse ring
const pr=r+5+3*Math.sin(t*0.12+p.bob*2);
ctx.beginPath();ctx.arc(0,0,pr,0,Math.PI*2);
ctx.strokeStyle=cfg.col+'44';ctx.lineWidth=2;ctx.stroke();
ctx.beginPath();
if(cfg.shape==='tri'){
ctx.moveTo(0,-r);ctx.lineTo(r*.87,r*.5);ctx.lineTo(-r*.87,r*.5);ctx.closePath();
} else if(cfg.shape==='hex'){
for(let i=0;i<6;i++){const a=i*Math.PI/3-Math.PI/6;ctx.lineTo(Math.cos(a)*r,Math.sin(a)*r);}ctx.closePath();
} else if(cfg.shape==='star5'){
for(let i=0;i<10;i++){const ir=i%2===0?r:r*.45,a=i*Math.PI/5-Math.PI/2;i===0?ctx.moveTo(Math.cos(a)*ir,Math.sin(a)*ir):ctx.lineTo(Math.cos(a)*ir,Math.sin(a)*ir);}ctx.closePath();
} else {
ctx.arc(0,0,r,0,Math.PI*2);
}
ctx.fillStyle=cfg.col+'cc';ctx.fill();
ctx.strokeStyle='rgba(255,255,255,0.7)';ctx.lineWidth=1.2;ctx.stroke();
ctx.fillStyle='#fff';ctx.font=`bold 9px monospace`;ctx.textAlign='center';ctx.fillText(cfg.label,0,3.5);
ctx.restore();
}
// ── Draw bullets ──────────────────────────────────────────────────────────────
function drawEBullet(bl){
const frozen=timers.freeze>0,slow=timers.slow>0;
ctx.save();ctx.translate(bl.x,bl.y);ctx.rotate(Math.atan2(bl.vy,bl.vx));
ctx.beginPath();ctx.ellipse(0,0,7,3.2,0,0,Math.PI*2);
ctx.fillStyle=frozen?'rgba(80,150,255,0.35)':slow?'rgba(255,200,60,0.55)':'#f7c948';ctx.fill();
if(!frozen&&!slow){ctx.beginPath();ctx.arc(4.5,0,2,0,Math.PI*2);ctx.fillStyle='#fff';ctx.fill();}
ctx.restore();
}
function drawPBullet(bl){
ctx.save();ctx.translate(bl.x,bl.y);
ctx.beginPath();ctx.moveTo(-24,0);ctx.lineTo(-6,0);
ctx.strokeStyle='rgba(212,107,255,0.28)';ctx.lineWidth=5;ctx.stroke();
ctx.beginPath();ctx.ellipse(0,0,9,3.5,0,0,Math.PI*2);ctx.fillStyle='#d46bff';ctx.fill();
ctx.beginPath();ctx.arc(7,0,3,0,Math.PI*2);ctx.fillStyle='#fff';ctx.fill();
ctx.restore();
}
// ── Draw particles & floats ───────────────────────────────────────────────────
function drawParticles(){
for(let i=particles.length-1;i>=0;i--){
const p=particles[i];
ctx.globalAlpha=p.life;ctx.beginPath();ctx.arc(p.x,p.y,p.r*p.life,0,Math.PI*2);
ctx.fillStyle=p.col;ctx.fill();
p.x+=p.vx;p.y+=p.vy;p.vx*=0.9;p.vy*=0.9;p.life-=0.03;
if(p.life<=0)particles.splice(i,1);
}
ctx.globalAlpha=1;
}
function drawFloatTexts(){
for(let i=floatTexts.length-1;i>=0;i--){
const f=floatTexts[i];
ctx.globalAlpha=f.life;
ctx.font=`bold ${f.size}px 'Share Tech Mono',monospace`;
ctx.textAlign='center';ctx.fillStyle=f.col;
ctx.shadowColor=f.col;ctx.shadowBlur=8;
ctx.fillText(f.txt,f.x,f.y);
ctx.shadowBlur=0;
f.y+=f.vy;f.life-=0.025;
if(f.life<=0)floatTexts.splice(i,1);
}
ctx.globalAlpha=1;ctx.textAlign='left';
}
// ── Nuke ─────────────────────────────────────────────────────────────────────
function doNuke(){
enemies.forEach(e=>{burst(e.x,e.y,'#ff4466',20,6);burst(e.x,e.y,'#fff',8,4);});
bullets.forEach(bl=>{burst(bl.x,bl.y,'#f7c948',4);});
const killed=enemies.length;
enemies.length=0;bullets.length=0;
if(killed>0)addPoints(killed*2,W/2,H/2,'💣 NUKE! +'+(killed*2*comboMult));
floatText(W/2,H/2-30,'💣 NUKE!','#ff4466',24);
// Flash
ctx.save();ctx.globalAlpha=0.4;ctx.fillStyle='#ff4466';ctx.fillRect(0,0,W,H);ctx.restore();
}
// ── Activate power-up ─────────────────────────────────────────────────────────
function activatePU(type,px,py){
const colMap={boost:'#3ecfcf',heart:'#f76464',freeze:'#5bb8ff',gun:'#d46bff',
slow:'#ffcc44',tiny:'#44ffaa',magnet:'#aa88ff',nuke:'#ff4466'};
burst(px,py,colMap[type]||'#fff',16,5);
if(type==='boost'){
timers.boost=DUR.boost;shield=Math.min(100,shield+45);updShield();
floatText(px,py-20,'⚡ BOOST!','#3ecfcf');
} else if(type==='heart'){
const maxL=MAX_LIVES+(selectedChar.passive==='extra_lives'?2:0);
lives=Math.min(maxL,lives+1);updLives();floatText(px,py-20,'❤ +1 LIFE','#f76464');
} else if(type==='freeze'){
timers.freeze=DUR.freeze;floatText(px,py-20,'❄ FREEZE!','#5bb8ff');
} else if(type==='gun'){
const dur=selectedChar.passive==='gun_duration'?DUR.gun*2:DUR.gun;
timers.gun=dur;floatText(px,py-20,'☆ GUN!','#d46bff');
} else if(type==='slow'){
timers.slow=DUR.slow;floatText(px,py-20,'⏳ SLOW-MO!','#ffcc44');
} else if(type==='tiny'){
const dur=selectedChar.passive==='phase'?DUR.tiny*1.67:DUR.tiny;
timers.tiny=Math.round(dur);floatText(px,py-20,'🔬 TINY!','#44ffaa');
} else if(type==='magnet'){
timers.magnet=DUR.magnet;floatText(px,py-20,'🧲 MAGNET!','#aa88ff');
} else if(type==='nuke'){
doNuke();
}
updSlots();
}
// ── HUD overlay ───────────────────────────────────────────────────────────────
function drawHUD(t){
// Score
ctx.font=`700 28px 'Orbitron',monospace`;
ctx.textAlign='center';
ctx.fillStyle=`hsla(${t%360},70%,80%,0.9)`;
ctx.fillText(score,W/2,38);
// Active power labels top-right
const labels=[
{k:'boost',col:'#3ecfcf',lbl:'⚡'},
{k:'freeze',col:'#5bb8ff',lbl:'❄'},
{k:'gun',col:'#d46bff',lbl:'☆'},
{k:'slow',col:'#ffcc44',lbl:'⏳'},
{k:'tiny',col:'#44ffaa',lbl:'🔬'},
{k:'magnet',col:'#aa88ff',lbl:'🧲'},
];
ctx.font='bold 10px monospace';ctx.textAlign='right';
let ry=18;
labels.forEach(({k,col,lbl})=>{
if(timers[k]>0){
ctx.fillStyle=col;
ctx.fillText(lbl+' '+Math.ceil(timers[k]/60)+'s',W-10,ry);
ry+=14;
}
});
ctx.textAlign='left';
// Stomp flash
if(stompFlash>0){
const a=stompFlash/38,yo=(38-stompFlash)*1.8;
ctx.save();ctx.globalAlpha=a;
ctx.font=`bold 22px 'Orbitron',monospace`;ctx.textAlign='center';
ctx.shadowColor='#ffe066';ctx.shadowBlur=15;
ctx.fillStyle='#ffe066';ctx.fillText('★ STOMP!',W/2,H/2-45-yo);
ctx.restore();stompFlash--;
}
// Combo flash
if(comboMult>=3){
const a=0.35+0.3*Math.sin(t*0.18);
ctx.save();ctx.globalAlpha=a;
ctx.font=`700 14px 'Orbitron',monospace`;ctx.textAlign='center';
ctx.fillStyle='#f7c948';ctx.fillText('× '+comboMult+' COMBO',W/2,H-16);
ctx.restore();
}
ctx.textAlign='left';
}
// ══════════════════════════════════════════════════════════════════════════════
// MAIN LOOP
// ══════════════════════════════════════════════════════════════════════════════
function loop(){
requestAnimationFrame(loop);
const t=gframe;
if(state==='idle'){
drawBg(t);
const iy=H/2+Math.sin(t*.055)*14;
const g=ctx.createRadialGradient(130,iy,1,130,iy,24);
g.addColorStop(0,selectedChar.glow);g.addColorStop(1,'transparent');
ctx.beginPath();ctx.arc(130,iy,24,0,Math.PI*2);ctx.fillStyle=g;ctx.fill();
selectedChar.draw(ctx,130,iy,t,selectedChar.r*1.4);
gframe++;return;
}
if(state==='dead'){drawBg(t);drawParticles();drawFloatTexts();return;}
// ── PLAY ──
gframe++;
const s=gameSpd();
// Tick timers
Object.keys(timers).forEach(k=>{if(timers[k]>0)timers[k]--;});
if(invT>0)invT--;
// Ghost shield regen
if(selectedChar.passive==='shield_regen'&&shield<100){
shieldRegenTimer++;
if(shieldRegenTimer%80===0){shield=Math.min(100,shield+4);updShield();}
}
// Combo decay
if(comboTimer>0){comboTimer--;if(comboTimer===0){combo=0;comboMult=1;}updCombo();}
// Bird
bird.vy+=held?GRAV-GLIDE:GRAV;
bird.vy=Math.max(-10,Math.min(bird.vy,12));
bird.y+=bird.vy;
bird.trail.push({x:bird.x,y:bird.y});
if(bird.trail.length>20)bird.trail.shift();
// Auto gun
const gunRate=selectedChar.passive==='gun_duration'?12:16;
if(timers.gun>0&&gframe%gunRate===0){
pBullets.push({x:bird.x+effectiveR()+20,y:bird.y,vx:13.5});
}
// Spawn events
if(gframe%PIPE_INT===0)spawnPipe();
if(gframe%210===0)spawnEnemy();
if(gframe%260===0)spawnPU();
// Bonus spawn when high score
if(score>40&&gframe%140===0)spawnEnemy();
// ── DRAW ──
drawBg(t);
// Pipes
for(let i=pipes.length-1;i>=0;i--){
pipes[i].x-=s;
if(!pipes[i].scored&&pipes[i].x+PIPE_W<bird.x){
pipes[i].scored=true;addPoints(1,bird.x,bird.y-30);
}
if(pipes[i].x+PIPE_W<-10){pipes.splice(i,1);continue;}
drawPipe(pipes[i],t);
if(colPipe({x:bird.x,y:bird.y,r:effectiveR()},pipes[i]))takeDamage(bird.x,bird.y);
}
// Enemies
for(let i=enemies.length-1;i>=0;i--){
const e=enemies[i];
const paused=timers.freeze>0;
const slowed=timers.slow>0;
e.x-=s*0.52;
if(!paused){e.y+=e.vy*(slowed?0.3:1);if(e.y<25||e.y>H-25)e.vy*=-1;}
// Shoot
if(!paused){
e.shootT-=(slowed?0.3:1);
if(e.shootT<=0){
const dx=bird.x-e.x,dy=bird.y-e.y,d=Math.hypot(dx,dy);
const bs=slowed?1.5:4.6;
bullets.push({x:e.x-e.r,y:e.y,vx:(dx/d)*bs,vy:(dy/d)*bs});
e.shootT=rnd(60,115);burst(e.x,e.y,'#f7c948',3,2);
}
}
if(e.x<-30){enemies.splice(i,1);continue;}
drawEnemy(e,t);
// Collision
if(circ(bird.x,bird.y,effectiveR(),e.x,e.y,e.r*1.15)){
const isStomp=bird.vy>(selectedChar.passive==='extra_lives'?0.7:1.5)&&bird.y<e.y+e.r*.5;
if(isStomp){
burst(e.x,e.y,'#ffe066',18,5);burst(e.x,e.y,selectedChar.color,8,3);
enemies.splice(i,1);
const bounceStr=selectedChar.passive==='extra_lives'?0.88:0.72;
bird.vy=JUMP*bounceStr;stompFlash=38;
const pts=selectedChar.passive==='extra_lives'?3:2;
addPoints(pts,e.x,e.y-20,'★ STOMP');
} else {
takeDamage(e.x,e.y);
}
}
}
// Enemy bullets
for(let i=bullets.length-1;i>=0;i--){
const bl=bullets[i];
if(timers.freeze===0){
const slowF=timers.slow>0?0.22:1;
bl.x+=bl.vx*slowF;bl.y+=bl.vy*slowF;
}
if(bl.x<-10||bl.x>W+10||bl.y<-10||bl.y>H+10){bullets.splice(i,1);continue;}
drawEBullet(bl);
if(timers.freeze===0&&circ(bird.x,bird.y,effectiveR(),bl.x,bl.y,5)){
bullets.splice(i,1);takeDamage(bl.x,bl.y);
}
}
// Player bullets
for(let i=pBullets.length-1;i>=0;i--){
const bl=pBullets[i];bl.x+=bl.vx;
if(bl.x>W+20){pBullets.splice(i,1);continue;}
drawPBullet(bl);
let hit=false;
for(let j=enemies.length-1;j>=0;j--){
if(circ(bl.x,bl.y,5,enemies[j].x,enemies[j].y,enemies[j].r*1.3)){
burst(enemies[j].x,enemies[j].y,'#d46bff',10);
enemies[j].hp--;
if(enemies[j].hp<=0){addPoints(1,enemies[j].x,enemies[j].y);enemies.splice(j,1);}
hit=true;break;
}
}
if(hit)pBullets.splice(i,1);
}
// Power-ups
for(let i=powerups.length-1;i>=0;i--){
const p=powerups[i];
if(timers.magnet>0){
const dx=bird.x-p.x,dy=bird.y-p.y,d=Math.hypot(dx,dy);
if(d<170){p.x+=dx*0.06;p.y+=dy*0.06;}
} else {
p.x-=s*0.44;
}
if(p.x<-28){powerups.splice(i,1);continue;}
drawPU(p,t);
const py=p.y+Math.sin(p.bob)*6;
if(circ(bird.x,bird.y,effectiveR(),p.x,py,p.r)){
activatePU(p.type,p.x,py);
powerups.splice(i,1);
}
}
drawParticles();
drawFloatTexts();
drawBird(bird,t);
// Wall
if(bird.y-effectiveR()<0||bird.y+effectiveR()>H){
takeDamage(bird.x,bird.y);
bird.vy*=-0.5;
bird.y=Math.max(effectiveR(),Math.min(H-effectiveR(),bird.y));
}
drawHUD(t);
updSlots();
}
</script>
</body>
</html>
"""
components.html(GAME_HTML, height=610, scrolling=False)
st.markdown("""
<div style="text-align:center;margin-top:6px;font-family:'Courier New',monospace;font-size:11px;color:#445;letter-spacing:1px;">
⚡ NOVA &nbsp;·&nbsp; 🔥 BLAZE &nbsp;·&nbsp; 👻 GHOST &nbsp;·&nbsp; ⚡ STORM &nbsp;·&nbsp; 🌀 V0ID
&nbsp;&nbsp;|&nbsp;&nbsp;
<span style="color:#3ecf8e">⚡BOOST</span> &nbsp;
<span style="color:#5bb8ff">❄FREEZE</span> &nbsp;
<span style="color:#d46bff">☆GUN</span> &nbsp;
<span style="color:#ffcc44">⏳SLOW</span> &nbsp;
<span style="color:#44ffaa">🔬TINY</span> &nbsp;
<span style="color:#aa88ff">🧲MAGNET</span> &nbsp;
<span style="color:#ff4466">💣NUKE</span>
&nbsp;&nbsp;|&nbsp;&nbsp;
<span style="color:#ffe066">★ STOMP aliens · 🔥 Chain combos</span>
</div>
""", unsafe_allow_html=True)