| 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> Best:<b id="hi" style="color:#a99ff7"> 0</b></span> |
| <span>♥ <span id="lives-bar"></span> |
| 🛡<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 | 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> 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 · 🔥 BLAZE · 👻 GHOST · ⚡ STORM · 🌀 V0ID |
| | |
| <span style="color:#3ecf8e">⚡BOOST</span> |
| <span style="color:#5bb8ff">❄FREEZE</span> |
| <span style="color:#d46bff">☆GUN</span> |
| <span style="color:#ffcc44">⏳SLOW</span> |
| <span style="color:#44ffaa">🔬TINY</span> |
| <span style="color:#aa88ff">🧲MAGNET</span> |
| <span style="color:#ff4466">💣NUKE</span> |
| | |
| <span style="color:#ffe066">★ STOMP aliens · 🔥 Chain combos</span> |
| </div> |
| """, unsafe_allow_html=True) |