dwishank commited on
Commit
70d5bf5
Β·
verified Β·
1 Parent(s): e60ea2c

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +304 -58
app.py CHANGED
@@ -1,8 +1,6 @@
1
  import streamlit as st
2
  import streamlit.components.v1 as components
3
-
4
  st.set_page_config(page_title="Void Dash", page_icon="⚑", layout="wide")
5
-
6
  st.markdown("""
7
  <style>
8
  body, .stApp { background:#05070f !important; }
@@ -12,7 +10,6 @@ h1 { color:#a99ff7; font-family:monospace; text-align:center; margin:0; }
12
  p { color:#7a7f94; text-align:center; font-family:monospace; font-size:13px; }
13
  </style>
14
  """, unsafe_allow_html=True)
15
-
16
  st.markdown("<h1>⚑ VOID DASH</h1>", unsafe_allow_html=True)
17
  st.markdown("<p>Tap / Space = jump &nbsp;|&nbsp; Hold = glide &nbsp;|&nbsp; Grab cyan boost &nbsp;|&nbsp; Dodge alien bullets!</p>", unsafe_allow_html=True)
18
 
@@ -26,7 +23,7 @@ body { background:#05070f; display:flex; flex-direction:column; align-items:cent
26
  canvas { display:block; border-radius:12px; cursor:pointer; }
27
  #hud {
28
  display:flex; justify-content:space-between; align-items:center;
29
- width:600px; padding:6px 10px; font-family:monospace; font-size:13px;
30
  color:#ccc; background:#0a0c14; border-radius:0 0 10px 10px;
31
  }
32
  #lives-bar span { display:inline-block; width:10px; height:10px; border-radius:50%; background:#f76464; margin:0 2px; }
@@ -35,25 +32,66 @@ canvas { display:block; border-radius:12px; cursor:pointer; }
35
  #wrap { position:relative; }
36
  #msg {
37
  position:absolute; top:50%; left:50%; transform:translate(-50%,-50%);
38
- color:#fff; text-align:center; pointer-events:none; min-width:220px;
39
  font-family:monospace;
40
  }
41
  #msg h2 { font-size:22px; font-weight:500; margin:0 0 6px; }
42
  #msg p { font-size:12px; color:#888; margin:2px 0; }
43
  .tag { display:inline-block; font-size:11px; padding:2px 8px; border-radius:10px; margin:2px; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
44
  </style>
45
  </head>
46
  <body>
47
- <div id="wrap">
48
- <canvas id="gc" width="600" height="360"></canvas>
49
- <div id="msg">
50
- <h2 style="color:#a99ff7">VOID DASH</h2>
51
- <p>Tap / Space = jump &nbsp;|&nbsp; Hold = glide</p>
52
- <p style="margin-top:8px;">
53
- <span class="tag" style="background:#1a3a1a;color:#3ecf8e">BOOST = speed + shield</span>
54
- <span class="tag" style="background:#3a1a1a;color:#f76464">ALIENS fire bullets!</span>
55
- </p>
56
- <p style="margin-top:6px;color:#7c6af7;font-size:13px;">Tap to start</p>
 
 
 
 
 
 
 
 
 
 
 
57
  </div>
58
  </div>
59
  <div id="hud">
@@ -64,16 +102,50 @@ canvas { display:block; border-radius:12px; cursor:pointer; }
64
  </span>
65
  <span id="boost-txt" style="color:#3ecf8e;font-size:12px;min-width:60px;text-align:right;"></span>
66
  </div>
 
 
 
 
 
67
 
68
  <script>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
69
  const C=document.getElementById('gc'), ctx=C.getContext('2d');
70
  const W=C.width, H=C.height;
71
  const GRAV=0.38,JUMP=-7.2,GLIDE=0.14,PIPE_W=44,GAP=128,PIPE_SPD=2.8,PIPE_INT=185;
72
  const MAX_LIVES=3;
73
  let state='idle',frame=0,score=0,best=0,held=false;
74
- let bird,pipes,enemies,bullets,particles,powerups;
75
- let lives,shield,boostTimer,invTimer;
76
 
 
77
  function initLivesUI(){
78
  const b=document.getElementById('lives-bar'); b.innerHTML='';
79
  for(let i=0;i<MAX_LIVES;i++) b.appendChild(Object.assign(document.createElement('span'),{id:'life'+i}));
@@ -81,15 +153,26 @@ function initLivesUI(){
81
  function updateLivesUI(){ for(let i=0;i<MAX_LIVES;i++){ const s=document.getElementById('life'+i); if(s) s.style.background=i<lives?'#f76464':'#333'; } }
82
  function updateShieldUI(){ document.getElementById('shield-fill').style.width=Math.round(shield)+'%'; }
83
  function updateBoostUI(){ document.getElementById('boost-txt').textContent=boostTimer>0?'BOOST '+Math.ceil(boostTimer/60)+'s':''; }
 
 
 
 
 
 
 
 
 
84
 
 
85
  function init(){
86
- bird={x:110,y:H/2,vy:0,r:10,trail:[]};
87
- pipes=[];enemies=[];bullets=[];particles=[];powerups=[];
88
- frame=0;score=0;lives=MAX_LIVES;shield=0;boostTimer=0;invTimer=0;
89
  document.getElementById('sc').textContent=0;
90
- updateLivesUI();updateShieldUI();updateBoostUI();
91
  }
92
 
 
93
  function jump(){
94
  if(state==='idle'||state==='dead'){state='play';document.getElementById('msg').style.display='none';init();}
95
  if(state==='play') bird.vy=JUMP;
@@ -102,14 +185,28 @@ C.addEventListener('touchend',e=>{e.preventDefault();release();},{passive:false}
102
  window.addEventListener('keydown',e=>{if(e.code==='Space'){e.preventDefault();jump();}});
103
  window.addEventListener('keyup',e=>{if(e.code==='Space')release();});
104
 
 
105
  function rnd(a,b){return a+Math.random()*(b-a);}
106
  function hsl(h,s,l){return `hsl(${h},${s}%,${l}%)`;}
 
 
107
 
 
108
  function spawnPipe(){ const top=55+Math.random()*(H-GAP-95); pipes.push({x:W+10,top,scored:false}); }
109
  function spawnEnemy(){ enemies.push({x:W+20,y:rnd(30,H-30),vy:rnd(-0.6,0.6),r:14,shootTimer:rnd(40,100),hp:2,frame:0}); }
110
- function spawnPowerup(){ powerups.push({x:W+10,y:rnd(50,H-50),type:Math.random()<0.6?'boost':'heart',r:12,bob:Math.random()*Math.PI*2}); }
 
 
 
 
 
 
 
 
 
111
  function spawnParticles(x,y,color,n=8){ for(let i=0;i<n;i++){ const a=Math.random()*Math.PI*2,spd=rnd(1.5,4.5); particles.push({x,y,vx:Math.cos(a)*spd,vy:Math.sin(a)*spd,life:1,color}); } }
112
 
 
113
  function drawBird(b,t){
114
  for(let i=0;i<b.trail.length;i++){
115
  const pt=b.trail[i],a=(i/b.trail.length)*0.3;
@@ -117,26 +214,48 @@ function drawBird(b,t){
117
  ctx.fillStyle=`hsla(${(t*1.5+i*5)%360},90%,65%,${a})`;ctx.fill();
118
  }
119
  if(boostTimer>0){ctx.beginPath();ctx.arc(b.x,b.y,b.r*2.2,0,Math.PI*2);ctx.fillStyle=`rgba(62,207,207,${0.15+0.1*Math.sin(t*0.3)})`;ctx.fill();}
 
 
 
 
 
 
 
 
 
 
 
120
  if(invTimer>0&&Math.floor(invTimer/4)%2===0) return;
121
- ctx.beginPath();ctx.arc(b.x,b.y,b.r,0,Math.PI*2);ctx.fillStyle=boostTimer>0?'#3ecfcf':'#fff';ctx.fill();
 
122
  ctx.beginPath();ctx.arc(b.x-3,b.y-3,b.r*0.38,0,Math.PI*2);ctx.fillStyle='rgba(0,0,0,0.25)';ctx.fill();
123
  }
124
 
 
125
  function drawEnemy(e,t){
126
  e.frame++;
127
  const bob=Math.sin(e.frame*0.05)*3,bx=e.x,by=e.y+bob;
 
 
128
  ctx.save();ctx.translate(bx,by);
129
  ctx.beginPath();ctx.ellipse(0,0,e.r*1.3,e.r,0,0,Math.PI*2);
130
- ctx.fillStyle=hsl((t*0.8)%360,80,35);ctx.fill();
131
- ctx.strokeStyle=hsl((t*0.8+30)%360,90,60);ctx.lineWidth=1.5;ctx.stroke();
132
- ctx.fillStyle=hsl((t*1.2)%360,100,70);
133
- for(let i=-1;i<=1;i+=2){ctx.beginPath();ctx.arc(i*5,-3,3,0,Math.PI*2);ctx.fill();}
134
- ctx.strokeStyle='#f76464';ctx.lineWidth=1;
135
- for(let i=0;i<3;i++){ctx.beginPath();ctx.moveTo(-e.r*1.3+i*e.r*0.7,-e.r);ctx.lineTo(-e.r*1.3+i*e.r*0.7+3,-e.r-6);ctx.stroke();}
 
 
 
 
 
 
136
  ctx.restore();
137
- for(let h=0;h<2;h++){ctx.fillStyle=h<e.hp?'#f76464':'#333';ctx.fillRect(e.x-10+h*12,e.y+bob-e.r-10,10,4);}
138
  }
139
 
 
140
  function drawPowerup(p,t){
141
  p.bob+=0.06;const y=p.y+Math.sin(p.bob)*5;
142
  ctx.save();ctx.translate(p.x,y);
@@ -144,20 +263,56 @@ function drawPowerup(p,t){
144
  ctx.beginPath();ctx.moveTo(0,-p.r);ctx.lineTo(p.r*0.8,p.r*0.5);ctx.lineTo(-p.r*0.8,p.r*0.5);ctx.closePath();
145
  ctx.fillStyle='rgba(62,207,207,0.9)';ctx.fill();ctx.strokeStyle='#fff';ctx.lineWidth=1;ctx.stroke();
146
  ctx.fillStyle='#fff';ctx.font='bold 9px monospace';ctx.textAlign='center';ctx.fillText('B',0,4);
147
- } else {
148
  ctx.beginPath();ctx.arc(0,0,p.r,0,Math.PI*2);ctx.fillStyle='rgba(247,100,100,0.85)';ctx.fill();
149
  ctx.strokeStyle='#fff';ctx.lineWidth=1;ctx.stroke();
150
  ctx.fillStyle='#fff';ctx.font='bold 11px monospace';ctx.textAlign='center';ctx.fillText('+',0,4);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
151
  }
152
  ctx.restore();
153
  }
154
 
 
 
 
 
 
 
 
 
 
 
 
155
  function drawBullet(bl){
156
- ctx.beginPath();ctx.ellipse(bl.x,bl.y,6,3,Math.atan2(bl.vy,bl.vx),0,Math.PI*2);
157
- ctx.fillStyle='#f7c948';ctx.fill();
158
- ctx.beginPath();ctx.arc(bl.x,bl.y,2,0,Math.PI*2);ctx.fillStyle='#fff';ctx.fill();
 
 
 
 
 
 
 
159
  }
160
 
 
161
  function drawPipe(p,t){
162
  const g1=ctx.createLinearGradient(p.x,0,p.x+PIPE_W,0);
163
  g1.addColorStop(0,hsl((t*0.6)%360,60,25));g1.addColorStop(1,hsl((t*0.6+40)%360,60,40));
@@ -167,11 +322,18 @@ function drawPipe(p,t){
167
  ctx.strokeStyle=hsl((t*1.1)%360,90,60);ctx.lineWidth=1.2;ctx.stroke();
168
  }
169
 
 
170
  function drawGrid(t){
171
- ctx.strokeStyle='rgba(80,60,200,0.06)';ctx.lineWidth=0.5;
172
  const spd=boostTimer>0?PIPE_SPD*2:PIPE_SPD,off=(t*spd*0.35)%40;
173
  for(let x=-off;x<W;x+=40){ctx.beginPath();ctx.moveTo(x,0);ctx.lineTo(x,H);ctx.stroke();}
174
  for(let y=0;y<H;y+=40){ctx.beginPath();ctx.moveTo(0,y);ctx.lineTo(W,y);ctx.stroke();}
 
 
 
 
 
 
175
  }
176
  function drawStars(t){
177
  for(let i=0;i<55;i++){
@@ -179,6 +341,8 @@ function drawStars(t){
179
  ctx.fillStyle=`rgba(255,255,255,${a})`;ctx.fillRect(sx,sy,1.5,1.5);
180
  }
181
  }
 
 
182
  function drawParticles(){
183
  for(let i=particles.length-1;i>=0;i--){
184
  const p=particles[i];
@@ -189,22 +353,31 @@ function drawParticles(){
189
  }
190
  }
191
 
192
- function circle(ax,ay,ar,bx,by,br){return Math.hypot(ax-bx,ay-by)<ar+br;}
193
- function collidesPipe(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);}
194
-
195
  function takeDamage(x,y){
196
  if(invTimer>0) return;
197
  if(shield>=30){shield=Math.max(0,shield-30);updateShieldUI();spawnParticles(bird.x,bird.y,'#3ecfcf',6);return;}
198
  lives--;updateLivesUI();invTimer=90;spawnParticles(x,y,'#f76464',10);
199
  if(lives<=0) die();
200
  }
201
-
202
  function die(){
203
  state='dead';
 
 
 
204
  const msg=document.getElementById('msg');msg.style.display='';
205
- msg.innerHTML=`<h2 style="color:#f76464">DESTROYED</h2><p>Score: ${score} &nbsp;|&nbsp; Best: ${best}</p><p style="margin-top:6px;color:#7c6af7;font-size:13px;">Tap / Space to retry</p>`;
 
 
 
 
 
 
 
 
206
  }
207
 
 
208
  function loop(){
209
  requestAnimationFrame(loop);
210
  const t=frame;
@@ -223,19 +396,31 @@ function loop(){
223
  if(state==='play'){
224
  frame++;
225
  const spd=boostTimer>0?PIPE_SPD*1.8:PIPE_SPD;
226
- if(boostTimer>0){boostTimer--;if(frame%60===0||boostTimer===0) updateBoostUI();}
 
 
 
 
 
 
 
 
227
  if(invTimer>0) invTimer--;
 
228
 
 
229
  bird.vy+=held?GRAV-GLIDE:GRAV;
230
  bird.vy=Math.max(-10,Math.min(bird.vy,12));
231
  bird.y+=bird.vy;
232
  bird.trail.push({x:bird.x,y:bird.y});
233
  if(bird.trail.length>16) bird.trail.shift();
234
 
 
235
  if(frame%PIPE_INT===0) spawnPipe();
236
  if(frame%220===0) spawnEnemy();
237
- if(frame%300===0) spawnPowerup();
238
 
 
239
  for(let i=pipes.length-1;i>=0;i--){
240
  pipes[i].x-=spd;
241
  if(!pipes[i].scored&&pipes[i].x+PIPE_W<bird.x){
@@ -248,69 +433,130 @@ function loop(){
248
  if(collidesPipe(bird,pipes[i])) takeDamage(bird.x,bird.y);
249
  }
250
 
 
251
  for(let i=enemies.length-1;i>=0;i--){
252
  const e=enemies[i];
253
- e.x-=spd*0.55;e.y+=e.vy;
 
254
  if(e.y<20||e.y>H-20) e.vy*=-1;
255
- e.shootTimer--;
256
- if(e.shootTimer<=0){
257
- const dx=bird.x-e.x,dy=bird.y-e.y,dist=Math.hypot(dx,dy),s2=4.5;
258
- bullets.push({x:e.x-e.r,y:e.y,vx:(dx/dist)*s2,vy:(dy/dist)*s2});
259
- e.shootTimer=rnd(60,110);spawnParticles(e.x,e.y,'#f7c948',3);
 
 
 
 
260
  }
 
261
  if(e.x<-30){enemies.splice(i,1);continue;}
262
  drawEnemy(e,t);
263
  if(circle(bird.x,bird.y,bird.r,e.x,e.y,e.r*1.2)) takeDamage(e.x,e.y);
264
  }
265
 
 
266
  for(let i=bullets.length-1;i>=0;i--){
267
- const bl=bullets[i];bl.x+=bl.vx;bl.y+=bl.vy;
 
 
268
  if(bl.x<-10||bl.x>W+10||bl.y<-10||bl.y>H+10){bullets.splice(i,1);continue;}
269
  drawBullet(bl);
270
- if(circle(bird.x,bird.y,bird.r,bl.x,bl.y,5)){bullets.splice(i,1);takeDamage(bl.x,bl.y);}
271
  }
272
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
273
  for(let i=powerups.length-1;i>=0;i--){
274
  const p=powerups[i];p.x-=spd*0.5;
275
  if(p.x<-20){powerups.splice(i,1);continue;}
276
  drawPowerup(p,t);
277
  if(circle(bird.x,bird.y,bird.r,p.x,p.y+Math.sin(p.bob)*5,p.r)){
278
- if(p.type==='boost'){boostTimer=300;shield=Math.min(100,shield+40);spawnParticles(p.x,p.y,'#3ecfcf',12);updateShieldUI();updateBoostUI();}
279
- else{lives=Math.min(MAX_LIVES,lives+1);spawnParticles(p.x,p.y,'#f76464',10);updateLivesUI();}
 
 
 
 
 
 
 
 
 
 
280
  powerups.splice(i,1);
281
  }
282
  }
283
 
284
- drawParticles();drawBird(bird,t);
 
285
 
 
286
  if(bird.y-bird.r<0||bird.y+bird.r>H){takeDamage(bird.x,bird.y);bird.vy*=-0.5;bird.y=Math.max(bird.r,Math.min(H-bird.r,bird.y));}
287
 
 
288
  ctx.fillStyle=`hsla(${t%360},80%,80%,0.85)`;
289
  ctx.font='500 26px monospace';ctx.textAlign='center';
290
  ctx.fillText(score,W/2,40);ctx.textAlign='left';
291
 
292
  if(boostTimer>0){ctx.fillStyle='rgba(62,207,207,0.7)';ctx.font='bold 11px monospace';ctx.textAlign='right';ctx.fillText('BOOST',W-12,20);ctx.textAlign='left';}
 
 
 
 
 
 
 
 
293
  }
294
  }
295
 
296
- initLivesUI();init();loop();
297
  </script>
298
  </body>
299
  </html>
300
  """
301
 
302
- components.html(GAME_HTML, height=430, scrolling=False)
303
 
304
  st.markdown("""
305
  <div style="text-align:center;margin-top:8px;">
306
  <span style="background:#1a3a1a;color:#3ecf8e;padding:3px 10px;border-radius:10px;font-size:12px;font-family:monospace;margin:4px;">
307
- Cyan triangle = BOOST (speed + shield)
308
  </span>
309
  <span style="background:#3a1a1a;color:#f76464;padding:3px 10px;border-radius:10px;font-size:12px;font-family:monospace;margin:4px;">
310
- Red circle = +1 Life
 
 
 
 
 
 
311
  </span>
312
  <span style="background:#1a1a3a;color:#a99ff7;padding:3px 10px;border-radius:10px;font-size:12px;font-family:monospace;margin:4px;">
313
- Aliens fire aimed bullets!
314
  </span>
315
  </div>
316
  """, unsafe_allow_html=True)
 
1
  import streamlit as st
2
  import streamlit.components.v1 as components
 
3
  st.set_page_config(page_title="Void Dash", page_icon="⚑", layout="wide")
 
4
  st.markdown("""
5
  <style>
6
  body, .stApp { background:#05070f !important; }
 
10
  p { color:#7a7f94; text-align:center; font-family:monospace; font-size:13px; }
11
  </style>
12
  """, unsafe_allow_html=True)
 
13
  st.markdown("<h1>⚑ VOID DASH</h1>", unsafe_allow_html=True)
14
  st.markdown("<p>Tap / Space = jump &nbsp;|&nbsp; Hold = glide &nbsp;|&nbsp; Grab cyan boost &nbsp;|&nbsp; Dodge alien bullets!</p>", unsafe_allow_html=True)
15
 
 
23
  canvas { display:block; border-radius:12px; cursor:pointer; }
24
  #hud {
25
  display:flex; justify-content:space-between; align-items:center;
26
+ width:760px; padding:6px 10px; font-family:monospace; font-size:13px;
27
  color:#ccc; background:#0a0c14; border-radius:0 0 10px 10px;
28
  }
29
  #lives-bar span { display:inline-block; width:10px; height:10px; border-radius:50%; background:#f76464; margin:0 2px; }
 
32
  #wrap { position:relative; }
33
  #msg {
34
  position:absolute; top:50%; left:50%; transform:translate(-50%,-50%);
35
+ color:#fff; text-align:center; pointer-events:none; min-width:260px;
36
  font-family:monospace;
37
  }
38
  #msg h2 { font-size:22px; font-weight:500; margin:0 0 6px; }
39
  #msg p { font-size:12px; color:#888; margin:2px 0; }
40
  .tag { display:inline-block; font-size:11px; padding:2px 8px; border-radius:10px; margin:2px; }
41
+
42
+ /* Leaderboard panel */
43
+ #lb-panel {
44
+ position:absolute; top:0; right:-210px; width:200px; height:100%;
45
+ background:#08091a; border-radius:12px; border:1px solid #1e1f3a;
46
+ font-family:monospace; padding:10px 8px; overflow:hidden;
47
+ }
48
+ #lb-panel h3 { color:#a99ff7; font-size:13px; text-align:center; margin-bottom:8px; letter-spacing:2px; }
49
+ .lb-row { display:flex; justify-content:space-between; align-items:center;
50
+ padding:3px 6px; border-radius:6px; margin:2px 0; font-size:11px; }
51
+ .lb-row.gold { background:#2a2000; color:#f7c948; }
52
+ .lb-row.silver { background:#1a1a28; color:#b0b8d0; }
53
+ .lb-row.bronze { background:#1e1208; color:#cd7f32; }
54
+ .lb-row.normal { background:#0e0f1f; color:#7a7f94; }
55
+ .lb-row.highlight { border:1px solid #a99ff7; color:#a99ff7 !important; background:#14103a !important; }
56
+ .lb-rank { width:20px; text-align:center; font-weight:bold; }
57
+ .lb-score { font-weight:bold; }
58
+ .lb-empty { color:#333; font-size:11px; text-align:center; padding:20px 0; }
59
+
60
+ /* Power-up indicator bar */
61
+ #powerup-bar {
62
+ display:flex; gap:6px; justify-content:center;
63
+ width:760px; padding:4px 0; font-family:monospace; font-size:11px;
64
+ }
65
+ .pu-slot { display:flex; align-items:center; gap:4px; padding:3px 10px;
66
+ border-radius:8px; background:#0a0c14; color:#555; transition:all 0.3s; }
67
+ .pu-slot.active { color:#fff; }
68
+ .pu-slot.freeze.active { background:#0a1a2a; color:#5bb8ff; border:1px solid #3a7acf; }
69
+ .pu-slot.gun.active { background:#1a0a2a; color:#d46bff; border:1px solid #8a3acf; }
70
+ .pu-slot.boost.active { background:#0a2a1a; color:#3ecf8e; border:1px solid #3acf8e; }
71
  </style>
72
  </head>
73
  <body>
74
+ <div style="display:flex;align-items:flex-start;gap:0;">
75
+ <div id="wrap">
76
+ <canvas id="gc" width="760" height="400"></canvas>
77
+ <div id="msg">
78
+ <h2 style="color:#a99ff7">VOID DASH</h2>
79
+ <p>Tap / Space = jump &nbsp;|&nbsp; Hold = glide</p>
80
+ <p style="margin-top:6px;">
81
+ <span class="tag" style="background:#1a3a1a;color:#3ecf8e">BOOST = speed + shield</span>
82
+ <span class="tag" style="background:#3a1a1a;color:#f76464">ALIENS fire bullets!</span>
83
+ </p>
84
+ <p style="margin-top:4px;">
85
+ <span class="tag" style="background:#0a1a2a;color:#5bb8ff">❄ FREEZE = stop bullets 5s</span>
86
+ <span class="tag" style="background:#1a0a2a;color:#d46bff">β˜† GUN = shoot aliens 5s</span>
87
+ </p>
88
+ <p style="margin-top:6px;color:#7c6af7;font-size:13px;">Tap to start</p>
89
+ </div>
90
+ <!-- Leaderboard -->
91
+ <div id="lb-panel">
92
+ <h3>πŸ† TOP 10</h3>
93
+ <div id="lb-list"><div class="lb-empty">No scores yet</div></div>
94
+ </div>
95
  </div>
96
  </div>
97
  <div id="hud">
 
102
  </span>
103
  <span id="boost-txt" style="color:#3ecf8e;font-size:12px;min-width:60px;text-align:right;"></span>
104
  </div>
105
+ <div id="powerup-bar">
106
+ <div class="pu-slot boost" id="slot-boost">⚑ BOOST <span id="txt-boost"></span></div>
107
+ <div class="pu-slot freeze" id="slot-freeze">❄ FREEZE <span id="txt-freeze"></span></div>
108
+ <div class="pu-slot gun" id="slot-gun">β˜† GUN <span id="txt-gun"></span></div>
109
+ </div>
110
 
111
  <script>
112
+ // ─── Storage helpers ─────────────────────────────────────────────────────────
113
+ const LS_KEY = 'voidDashLeaderboard_v2';
114
+ function loadScores(){ try{ return JSON.parse(localStorage.getItem(LS_KEY))||[]; }catch(e){ return []; } }
115
+ function saveScores(arr){ try{ localStorage.setItem(LS_KEY,JSON.stringify(arr)); }catch(e){} }
116
+ function addScore(s){
117
+ const arr=loadScores();
118
+ arr.push(s);
119
+ arr.sort((a,b)=>b-a);
120
+ const trimmed=arr.slice(0,10);
121
+ saveScores(trimmed);
122
+ return trimmed;
123
+ }
124
+ function renderLeaderboard(highlight){
125
+ const arr=loadScores();
126
+ const el=document.getElementById('lb-list');
127
+ if(!arr.length){ el.innerHTML='<div class="lb-empty">Play to set a score!</div>'; return; }
128
+ el.innerHTML=arr.map((s,i)=>{
129
+ let cls='normal';
130
+ if(i===0) cls='gold'; else if(i===1) cls='silver'; else if(i===2) cls='bronze';
131
+ if(highlight!==undefined&&s===highlight&&i===arr.indexOf(highlight)) cls+=' highlight';
132
+ const medals=['πŸ₯‡','πŸ₯ˆ','πŸ₯‰'];
133
+ const rank=i<3?medals[i]:`${i+1}.`;
134
+ return `<div class="lb-row ${cls}"><span class="lb-rank">${rank}</span><span class="lb-score">${s}</span></div>`;
135
+ }).join('');
136
+ }
137
+ renderLeaderboard();
138
+
139
+ // ─── Canvas setup ─────────────────────────────────────────────────────────────
140
  const C=document.getElementById('gc'), ctx=C.getContext('2d');
141
  const W=C.width, H=C.height;
142
  const GRAV=0.38,JUMP=-7.2,GLIDE=0.14,PIPE_W=44,GAP=128,PIPE_SPD=2.8,PIPE_INT=185;
143
  const MAX_LIVES=3;
144
  let state='idle',frame=0,score=0,best=0,held=false;
145
+ let bird,pipes,enemies,bullets,playerBullets,particles,powerups;
146
+ let lives,shield,boostTimer,invTimer,freezeTimer,gunTimer;
147
 
148
+ // ─── HUD helpers ──────────────────────────────────────────────────────────────
149
  function initLivesUI(){
150
  const b=document.getElementById('lives-bar'); b.innerHTML='';
151
  for(let i=0;i<MAX_LIVES;i++) b.appendChild(Object.assign(document.createElement('span'),{id:'life'+i}));
 
153
  function updateLivesUI(){ for(let i=0;i<MAX_LIVES;i++){ const s=document.getElementById('life'+i); if(s) s.style.background=i<lives?'#f76464':'#333'; } }
154
  function updateShieldUI(){ document.getElementById('shield-fill').style.width=Math.round(shield)+'%'; }
155
  function updateBoostUI(){ document.getElementById('boost-txt').textContent=boostTimer>0?'BOOST '+Math.ceil(boostTimer/60)+'s':''; }
156
+ function updatePUSlots(){
157
+ const bs=document.getElementById('slot-boost'), fs=document.getElementById('slot-freeze'), gs=document.getElementById('slot-gun');
158
+ if(boostTimer>0){ bs.classList.add('active'); document.getElementById('txt-boost').textContent=Math.ceil(boostTimer/60)+'s'; }
159
+ else{ bs.classList.remove('active'); document.getElementById('txt-boost').textContent=''; }
160
+ if(freezeTimer>0){ fs.classList.add('active'); document.getElementById('txt-freeze').textContent=Math.ceil(freezeTimer/60)+'s'; }
161
+ else{ fs.classList.remove('active'); document.getElementById('txt-freeze').textContent=''; }
162
+ if(gunTimer>0){ gs.classList.add('active'); document.getElementById('txt-gun').textContent=Math.ceil(gunTimer/60)+'s'; }
163
+ else{ gs.classList.remove('active'); document.getElementById('txt-gun').textContent=''; }
164
+ }
165
 
166
+ // ─── Init ─────────────────────────────────────────────────────────────────────
167
  function init(){
168
+ bird={x:110,y:H/2,vy:0,r:10,trail:[],shootCooldown:0};
169
+ pipes=[];enemies=[];bullets=[];playerBullets=[];particles=[];powerups=[];
170
+ frame=0;score=0;lives=MAX_LIVES;shield=0;boostTimer=0;invTimer=0;freezeTimer=0;gunTimer=0;
171
  document.getElementById('sc').textContent=0;
172
+ updateLivesUI();updateShieldUI();updateBoostUI();updatePUSlots();
173
  }
174
 
175
+ // ─── Input ────────────────────────────────────────────────────────────────────
176
  function jump(){
177
  if(state==='idle'||state==='dead'){state='play';document.getElementById('msg').style.display='none';init();}
178
  if(state==='play') bird.vy=JUMP;
 
185
  window.addEventListener('keydown',e=>{if(e.code==='Space'){e.preventDefault();jump();}});
186
  window.addEventListener('keyup',e=>{if(e.code==='Space')release();});
187
 
188
+ // ─── Helpers ──────────────────────────────────────────────────────────────────
189
  function rnd(a,b){return a+Math.random()*(b-a);}
190
  function hsl(h,s,l){return `hsl(${h},${s}%,${l}%)`;}
191
+ function circle(ax,ay,ar,bx,by,br){return Math.hypot(ax-bx,ay-by)<ar+br;}
192
+ function collidesPipe(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);}
193
 
194
+ // ─── Spawners ─────────────────────────────────────────────────────────────────
195
  function spawnPipe(){ const top=55+Math.random()*(H-GAP-95); pipes.push({x:W+10,top,scored:false}); }
196
  function spawnEnemy(){ enemies.push({x:W+20,y:rnd(30,H-30),vy:rnd(-0.6,0.6),r:14,shootTimer:rnd(40,100),hp:2,frame:0}); }
197
+ function spawnPowerup(){
198
+ // Pick from 4 types
199
+ const roll=Math.random();
200
+ let type;
201
+ if(roll<0.3) type='boost';
202
+ else if(roll<0.55) type='heart';
203
+ else if(roll<0.78) type='freeze';
204
+ else type='gun';
205
+ powerups.push({x:W+10,y:rnd(50,H-50),type,r:13,bob:Math.random()*Math.PI*2});
206
+ }
207
  function spawnParticles(x,y,color,n=8){ for(let i=0;i<n;i++){ const a=Math.random()*Math.PI*2,spd=rnd(1.5,4.5); particles.push({x,y,vx:Math.cos(a)*spd,vy:Math.sin(a)*spd,life:1,color}); } }
208
 
209
+ // ─── Draw bird ────────────────────────────────────────────────────────────────
210
  function drawBird(b,t){
211
  for(let i=0;i<b.trail.length;i++){
212
  const pt=b.trail[i],a=(i/b.trail.length)*0.3;
 
214
  ctx.fillStyle=`hsla(${(t*1.5+i*5)%360},90%,65%,${a})`;ctx.fill();
215
  }
216
  if(boostTimer>0){ctx.beginPath();ctx.arc(b.x,b.y,b.r*2.2,0,Math.PI*2);ctx.fillStyle=`rgba(62,207,207,${0.15+0.1*Math.sin(t*0.3)})`;ctx.fill();}
217
+ if(gunTimer>0){
218
+ // Gun glow
219
+ ctx.beginPath();ctx.arc(b.x,b.y,b.r*2.5,0,Math.PI*2);
220
+ ctx.fillStyle=`rgba(212,107,255,${0.18+0.1*Math.sin(t*0.4)})`;ctx.fill();
221
+ // Draw gun barrel
222
+ ctx.save();ctx.translate(b.x+b.r,b.y);
223
+ ctx.fillStyle='#d46bff';ctx.fillRect(0,-3,18,6);
224
+ ctx.fillStyle='#fff';ctx.fillRect(16,-2,6,4);
225
+ ctx.restore();
226
+ }
227
+ if(freezeTimer>0){ctx.beginPath();ctx.arc(b.x,b.y,b.r*2,0,Math.PI*2);ctx.fillStyle=`rgba(91,184,255,${0.15+0.1*Math.sin(t*0.3)})`;ctx.fill();}
228
  if(invTimer>0&&Math.floor(invTimer/4)%2===0) return;
229
+ ctx.beginPath();ctx.arc(b.x,b.y,b.r,0,Math.PI*2);
230
+ ctx.fillStyle=boostTimer>0?'#3ecfcf':(gunTimer>0?'#d46bff':(freezeTimer>0?'#5bb8ff':'#fff'));ctx.fill();
231
  ctx.beginPath();ctx.arc(b.x-3,b.y-3,b.r*0.38,0,Math.PI*2);ctx.fillStyle='rgba(0,0,0,0.25)';ctx.fill();
232
  }
233
 
234
+ // ─── Draw enemy ───────────────────────────────────────────────────────────────
235
  function drawEnemy(e,t){
236
  e.frame++;
237
  const bob=Math.sin(e.frame*0.05)*3,bx=e.x,by=e.y+bob;
238
+ // Frozen overlay
239
+ const frozen=freezeTimer>0;
240
  ctx.save();ctx.translate(bx,by);
241
  ctx.beginPath();ctx.ellipse(0,0,e.r*1.3,e.r,0,0,Math.PI*2);
242
+ ctx.fillStyle=frozen?'#1a2a3a':hsl((t*0.8)%360,80,35);ctx.fill();
243
+ ctx.strokeStyle=frozen?'#5bb8ff':hsl((t*0.8+30)%360,90,60);ctx.lineWidth=1.5;ctx.stroke();
244
+ if(frozen){
245
+ // Ice crystal effect
246
+ ctx.strokeStyle='rgba(91,184,255,0.6)';ctx.lineWidth=1;
247
+ 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.1);ctx.stroke();ctx.restore();}
248
+ } else {
249
+ ctx.fillStyle=hsl((t*1.2)%360,100,70);
250
+ for(let i=-1;i<=1;i+=2){ctx.beginPath();ctx.arc(i*5,-3,3,0,Math.PI*2);ctx.fill();}
251
+ ctx.strokeStyle='#f76464';ctx.lineWidth=1;
252
+ for(let i=0;i<3;i++){ctx.beginPath();ctx.moveTo(-e.r*1.3+i*e.r*0.7,-e.r);ctx.lineTo(-e.r*1.3+i*e.r*0.7+3,-e.r-6);ctx.stroke();}
253
+ }
254
  ctx.restore();
255
+ for(let h=0;h<2;h++){ctx.fillStyle=h<e.hp?(frozen?'#5bb8ff':'#f76464'):'#333';ctx.fillRect(e.x-10+h*12,e.y+bob-e.r-10,10,4);}
256
  }
257
 
258
+ // ─── Draw powerup ─────────────────────────────────────────────────────────────
259
  function drawPowerup(p,t){
260
  p.bob+=0.06;const y=p.y+Math.sin(p.bob)*5;
261
  ctx.save();ctx.translate(p.x,y);
 
263
  ctx.beginPath();ctx.moveTo(0,-p.r);ctx.lineTo(p.r*0.8,p.r*0.5);ctx.lineTo(-p.r*0.8,p.r*0.5);ctx.closePath();
264
  ctx.fillStyle='rgba(62,207,207,0.9)';ctx.fill();ctx.strokeStyle='#fff';ctx.lineWidth=1;ctx.stroke();
265
  ctx.fillStyle='#fff';ctx.font='bold 9px monospace';ctx.textAlign='center';ctx.fillText('B',0,4);
266
+ } else if(p.type==='heart'){
267
  ctx.beginPath();ctx.arc(0,0,p.r,0,Math.PI*2);ctx.fillStyle='rgba(247,100,100,0.85)';ctx.fill();
268
  ctx.strokeStyle='#fff';ctx.lineWidth=1;ctx.stroke();
269
  ctx.fillStyle='#fff';ctx.font='bold 11px monospace';ctx.textAlign='center';ctx.fillText('+',0,4);
270
+ } else if(p.type==='freeze'){
271
+ // Snowflake / ice crystal
272
+ ctx.beginPath();ctx.arc(0,0,p.r,0,Math.PI*2);ctx.fillStyle='rgba(91,184,255,0.85)';ctx.fill();
273
+ ctx.strokeStyle='#fff';ctx.lineWidth=1;ctx.stroke();
274
+ ctx.strokeStyle='#fff';ctx.lineWidth=1.5;
275
+ for(let a=0;a<6;a++){ctx.save();ctx.rotate(a*Math.PI/3);ctx.beginPath();ctx.moveTo(0,0);ctx.lineTo(0,p.r*0.75);ctx.stroke();ctx.restore();}
276
+ ctx.fillStyle='#fff';ctx.font='bold 9px monospace';ctx.textAlign='center';ctx.fillText('❄',0,4);
277
+ } else if(p.type==='gun'){
278
+ // Star / gun power-up
279
+ const sp=p.r,si=sp*0.45,pts=5;
280
+ ctx.beginPath();
281
+ for(let i=0;i<pts*2;i++){
282
+ const r=i%2===0?sp:si,a=i*Math.PI/pts-Math.PI/2;
283
+ i===0?ctx.moveTo(Math.cos(a)*r,Math.sin(a)*r):ctx.lineTo(Math.cos(a)*r,Math.sin(a)*r);
284
+ }
285
+ ctx.closePath();ctx.fillStyle='rgba(212,107,255,0.9)';ctx.fill();ctx.strokeStyle='#fff';ctx.lineWidth=1;ctx.stroke();
286
+ ctx.fillStyle='#fff';ctx.font='bold 9px monospace';ctx.textAlign='center';ctx.fillText('β˜†',0,4);
287
  }
288
  ctx.restore();
289
  }
290
 
291
+ // ─── Draw player bullet ───────────────────────────────────────────────────────
292
+ function drawPlayerBullet(bl){
293
+ ctx.beginPath();ctx.ellipse(bl.x,bl.y,8,3,0,0,Math.PI*2);
294
+ ctx.fillStyle='#d46bff';ctx.fill();
295
+ ctx.beginPath();ctx.arc(bl.x+5,bl.y,2.5,0,Math.PI*2);ctx.fillStyle='#fff';ctx.fill();
296
+ // Trail
297
+ ctx.beginPath();ctx.moveTo(bl.x-6,bl.y);ctx.lineTo(bl.x-20,bl.y);
298
+ ctx.strokeStyle='rgba(212,107,255,0.35)';ctx.lineWidth=3;ctx.stroke();
299
+ }
300
+
301
+ // ─── Draw enemy bullet ────────────────────────────────────────────────────────
302
  function drawBullet(bl){
303
+ if(freezeTimer>0){
304
+ // Frozen bullet
305
+ ctx.beginPath();ctx.ellipse(bl.x,bl.y,6,3,Math.atan2(bl.vy,bl.vx),0,Math.PI*2);
306
+ ctx.fillStyle='rgba(91,184,255,0.5)';ctx.fill();
307
+ ctx.strokeStyle='#5bb8ff';ctx.lineWidth=1;ctx.stroke();
308
+ } else {
309
+ ctx.beginPath();ctx.ellipse(bl.x,bl.y,6,3,Math.atan2(bl.vy,bl.vx),0,Math.PI*2);
310
+ ctx.fillStyle='#f7c948';ctx.fill();
311
+ ctx.beginPath();ctx.arc(bl.x,bl.y,2,0,Math.PI*2);ctx.fillStyle='#fff';ctx.fill();
312
+ }
313
  }
314
 
315
+ // ─── Draw pipe ────────────────────────────────────────────────────────────────
316
  function drawPipe(p,t){
317
  const g1=ctx.createLinearGradient(p.x,0,p.x+PIPE_W,0);
318
  g1.addColorStop(0,hsl((t*0.6)%360,60,25));g1.addColorStop(1,hsl((t*0.6+40)%360,60,40));
 
322
  ctx.strokeStyle=hsl((t*1.1)%360,90,60);ctx.lineWidth=1.2;ctx.stroke();
323
  }
324
 
325
+ // ─── Draw BG ──────────────────────────────────────────────────────────────────
326
  function drawGrid(t){
327
+ ctx.strokeStyle=freezeTimer>0?'rgba(40,80,160,0.1)':'rgba(80,60,200,0.06)';ctx.lineWidth=0.5;
328
  const spd=boostTimer>0?PIPE_SPD*2:PIPE_SPD,off=(t*spd*0.35)%40;
329
  for(let x=-off;x<W;x+=40){ctx.beginPath();ctx.moveTo(x,0);ctx.lineTo(x,H);ctx.stroke();}
330
  for(let y=0;y<H;y+=40){ctx.beginPath();ctx.moveTo(0,y);ctx.lineTo(W,y);ctx.stroke();}
331
+ if(freezeTimer>0){
332
+ // Frost vignette
333
+ const g=ctx.createRadialGradient(W/2,H/2,H*0.3,W/2,H/2,H*0.8);
334
+ g.addColorStop(0,'transparent');g.addColorStop(1,'rgba(91,184,255,0.07)');
335
+ ctx.fillStyle=g;ctx.fillRect(0,0,W,H);
336
+ }
337
  }
338
  function drawStars(t){
339
  for(let i=0;i<55;i++){
 
341
  ctx.fillStyle=`rgba(255,255,255,${a})`;ctx.fillRect(sx,sy,1.5,1.5);
342
  }
343
  }
344
+
345
+ // ─── Draw particles ───────────────────────────────────────────────────────────
346
  function drawParticles(){
347
  for(let i=particles.length-1;i>=0;i--){
348
  const p=particles[i];
 
353
  }
354
  }
355
 
356
+ // ─── Damage / death ───────────────────────────────────────────────────────────
 
 
357
  function takeDamage(x,y){
358
  if(invTimer>0) return;
359
  if(shield>=30){shield=Math.max(0,shield-30);updateShieldUI();spawnParticles(bird.x,bird.y,'#3ecfcf',6);return;}
360
  lives--;updateLivesUI();invTimer=90;spawnParticles(x,y,'#f76464',10);
361
  if(lives<=0) die();
362
  }
 
363
  function die(){
364
  state='dead';
365
+ // Save score to leaderboard
366
+ const scores=addScore(score);
367
+ renderLeaderboard(score);
368
  const msg=document.getElementById('msg');msg.style.display='';
369
+ msg.innerHTML=`<h2 style="color:#f76464">DESTROYED</h2><p>Score: ${score} &nbsp;|&nbsp; Best: ${best}</p><p style="color:#a99ff7;font-size:11px;margin-top:4px;">Check leaderboard β†’</p><p style="margin-top:6px;color:#7c6af7;font-size:13px;">Tap / Space to retry</p>`;
370
+ }
371
+
372
+ // ─── FREEZE power-up: freeze all bullets in place ─────────────────────────────
373
+ // Handled in loop: bullets don't move when freezeTimer > 0
374
+
375
+ // ─── GUN: auto-shoot player bullets ──────────────────────────────────────────
376
+ function shootPlayerBullet(){
377
+ playerBullets.push({x:bird.x+bird.r+18,y:bird.y,vx:12,vy:0});
378
  }
379
 
380
+ // ─── Main loop ────────────────────────────────────────────────────────────────
381
  function loop(){
382
  requestAnimationFrame(loop);
383
  const t=frame;
 
396
  if(state==='play'){
397
  frame++;
398
  const spd=boostTimer>0?PIPE_SPD*1.8:PIPE_SPD;
399
+
400
+ // Tick timers
401
+ if(boostTimer>0){boostTimer--;}
402
+ if(freezeTimer>0){freezeTimer--;}
403
+ if(gunTimer>0){
404
+ gunTimer--;
405
+ // Auto-shoot every 18 frames
406
+ if(frame%18===0) shootPlayerBullet();
407
+ }
408
  if(invTimer>0) invTimer--;
409
+ updatePUSlots();
410
 
411
+ // Bird physics
412
  bird.vy+=held?GRAV-GLIDE:GRAV;
413
  bird.vy=Math.max(-10,Math.min(bird.vy,12));
414
  bird.y+=bird.vy;
415
  bird.trail.push({x:bird.x,y:bird.y});
416
  if(bird.trail.length>16) bird.trail.shift();
417
 
418
+ // Spawn
419
  if(frame%PIPE_INT===0) spawnPipe();
420
  if(frame%220===0) spawnEnemy();
421
+ if(frame%280===0) spawnPowerup();
422
 
423
+ // ── Pipes ──
424
  for(let i=pipes.length-1;i>=0;i--){
425
  pipes[i].x-=spd;
426
  if(!pipes[i].scored&&pipes[i].x+PIPE_W<bird.x){
 
433
  if(collidesPipe(bird,pipes[i])) takeDamage(bird.x,bird.y);
434
  }
435
 
436
+ // ── Enemies ──
437
  for(let i=enemies.length-1;i>=0;i--){
438
  const e=enemies[i];
439
+ e.x-=spd*0.55;
440
+ if(!freezeTimer){e.y+=e.vy;}
441
  if(e.y<20||e.y>H-20) e.vy*=-1;
442
+
443
+ // Enemy shooting β€” suppressed when frozen
444
+ if(!freezeTimer){
445
+ e.shootTimer--;
446
+ if(e.shootTimer<=0){
447
+ const dx=bird.x-e.x,dy=bird.y-e.y,dist=Math.hypot(dx,dy),s2=4.5;
448
+ bullets.push({x:e.x-e.r,y:e.y,vx:(dx/dist)*s2,vy:(dy/dist)*s2});
449
+ e.shootTimer=rnd(60,110);spawnParticles(e.x,e.y,'#f7c948',3);
450
+ }
451
  }
452
+
453
  if(e.x<-30){enemies.splice(i,1);continue;}
454
  drawEnemy(e,t);
455
  if(circle(bird.x,bird.y,bird.r,e.x,e.y,e.r*1.2)) takeDamage(e.x,e.y);
456
  }
457
 
458
+ // ── Enemy bullets ──
459
  for(let i=bullets.length-1;i>=0;i--){
460
+ const bl=bullets[i];
461
+ // Frozen: don't move bullets
462
+ if(!freezeTimer){bl.x+=bl.vx;bl.y+=bl.vy;}
463
  if(bl.x<-10||bl.x>W+10||bl.y<-10||bl.y>H+10){bullets.splice(i,1);continue;}
464
  drawBullet(bl);
465
+ if(!freezeTimer&&circle(bird.x,bird.y,bird.r,bl.x,bl.y,5)){bullets.splice(i,1);takeDamage(bl.x,bl.y);}
466
  }
467
 
468
+ // ── Player bullets (gun power-up) ──
469
+ for(let i=playerBullets.length-1;i>=0;i--){
470
+ const bl=playerBullets[i];
471
+ bl.x+=bl.vx;
472
+ if(bl.x>W+20){playerBullets.splice(i,1);continue;}
473
+ drawPlayerBullet(bl);
474
+ // Hit enemies
475
+ let hit=false;
476
+ for(let j=enemies.length-1;j>=0;j--){
477
+ if(circle(bl.x,bl.y,5,enemies[j].x,enemies[j].y,enemies[j].r*1.3)){
478
+ spawnParticles(enemies[j].x,enemies[j].y,'#d46bff',10);
479
+ enemies[j].hp--;
480
+ if(enemies[j].hp<=0){
481
+ score++;document.getElementById('sc').textContent=score;
482
+ if(score>best){best=score;document.getElementById('hi').textContent=best;}
483
+ enemies.splice(j,1);
484
+ }
485
+ hit=true;break;
486
+ }
487
+ }
488
+ if(hit){playerBullets.splice(i,1);}
489
+ }
490
+
491
+ // ── Powerups ──
492
  for(let i=powerups.length-1;i>=0;i--){
493
  const p=powerups[i];p.x-=spd*0.5;
494
  if(p.x<-20){powerups.splice(i,1);continue;}
495
  drawPowerup(p,t);
496
  if(circle(bird.x,bird.y,bird.r,p.x,p.y+Math.sin(p.bob)*5,p.r)){
497
+ if(p.type==='boost'){boostTimer=300;shield=Math.min(100,shield+40);spawnParticles(p.x,p.y,'#3ecfcf',12);updateShieldUI();}
498
+ else if(p.type==='heart'){lives=Math.min(MAX_LIVES,lives+1);spawnParticles(p.x,p.y,'#f76464',10);updateLivesUI();}
499
+ else if(p.type==='freeze'){
500
+ freezeTimer=300; // 5 seconds at 60fps
501
+ spawnParticles(p.x,p.y,'#5bb8ff',14);
502
+ // Clear frozen bullets visually (they stay but are harmless)
503
+ }
504
+ else if(p.type==='gun'){
505
+ gunTimer=300; // 5 seconds
506
+ spawnParticles(p.x,p.y,'#d46bff',14);
507
+ }
508
+ updatePUSlots();
509
  powerups.splice(i,1);
510
  }
511
  }
512
 
513
+ drawParticles();
514
+ drawBird(bird,t);
515
 
516
+ // Wall bounce
517
  if(bird.y-bird.r<0||bird.y+bird.r>H){takeDamage(bird.x,bird.y);bird.vy*=-0.5;bird.y=Math.max(bird.r,Math.min(H-bird.r,bird.y));}
518
 
519
+ // Score display on canvas
520
  ctx.fillStyle=`hsla(${t%360},80%,80%,0.85)`;
521
  ctx.font='500 26px monospace';ctx.textAlign='center';
522
  ctx.fillText(score,W/2,40);ctx.textAlign='left';
523
 
524
  if(boostTimer>0){ctx.fillStyle='rgba(62,207,207,0.7)';ctx.font='bold 11px monospace';ctx.textAlign='right';ctx.fillText('BOOST',W-12,20);ctx.textAlign='left';}
525
+ if(freezeTimer>0){
526
+ ctx.fillStyle='rgba(91,184,255,0.85)';ctx.font='bold 12px monospace';ctx.textAlign='right';
527
+ ctx.fillText('❄ FREEZE '+Math.ceil(freezeTimer/60)+'s',W-12,36);ctx.textAlign='left';
528
+ }
529
+ if(gunTimer>0){
530
+ ctx.fillStyle='rgba(212,107,255,0.85)';ctx.font='bold 12px monospace';ctx.textAlign='right';
531
+ ctx.fillText('β˜† GUN '+Math.ceil(gunTimer/60)+'s',W-12,52);ctx.textAlign='left';
532
+ }
533
  }
534
  }
535
 
536
+ initLivesUI();init();renderLeaderboard();loop();
537
  </script>
538
  </body>
539
  </html>
540
  """
541
 
542
+ components.html(GAME_HTML, height=510, scrolling=False)
543
 
544
  st.markdown("""
545
  <div style="text-align:center;margin-top:8px;">
546
  <span style="background:#1a3a1a;color:#3ecf8e;padding:3px 10px;border-radius:10px;font-size:12px;font-family:monospace;margin:4px;">
547
+ ⚑ Cyan triangle = BOOST (speed + shield)
548
  </span>
549
  <span style="background:#3a1a1a;color:#f76464;padding:3px 10px;border-radius:10px;font-size:12px;font-family:monospace;margin:4px;">
550
+ ❀ Red circle = +1 Life
551
+ </span>
552
+ <span style="background:#0a1a2a;color:#5bb8ff;padding:3px 10px;border-radius:10px;font-size:12px;font-family:monospace;margin:4px;">
553
+ ❄ Blue crystal = FREEZE (stop bullets 5s)
554
+ </span>
555
+ <span style="background:#1a0a2a;color:#d46bff;padding:3px 10px;border-radius:10px;font-size:12px;font-family:monospace;margin:4px;">
556
+ β˜† Purple star = GUN (shoot aliens 5s)
557
  </span>
558
  <span style="background:#1a1a3a;color:#a99ff7;padding:3px 10px;border-radius:10px;font-size:12px;font-family:monospace;margin:4px;">
559
+ πŸ† Leaderboard saves Top 10 scores!
560
  </span>
561
  </div>
562
  """, unsafe_allow_html=True)