Skip to content

Instantly share code, notes, and snippets.

@kumagi
Created September 23, 2026 01:46
Show Gist options
  • Select an option

  • Save kumagi/3983512dcb8d3b0192d7fd360386e1cd to your computer and use it in GitHub Desktop.

Select an option

Save kumagi/3983512dcb8d3b0192d7fd360386e1cd to your computer and use it in GitHub Desktop.
Model: MiMo-v2.6pro 時間: 36m14s
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<title>EXPLOSIVE TETRIS</title>
<style>
*{margin:0;padding:0;box-sizing:border-box}
html,body{width:100%;height:100%;background:#05060a;overflow:hidden;
font-family:'Segoe UI','Hiragino Sans','Noto Sans JP',system-ui,sans-serif;
color:#fff;user-select:none;-webkit-user-select:none;touch-action:none}
#stage{position:fixed;inset:0;display:flex;align-items:center;justify-content:center;
background:radial-gradient(ellipse at 50% 30%,#12162a 0%,#05060a 60%,#000 100%)}
#wrap{position:relative;z-index:1;filter:drop-shadow(0 20px 60px rgba(0,0,0,.8))}
canvas{display:block;border-radius:12px;background:transparent}
#tip{position:fixed;bottom:12px;left:50%;transform:translateX(-50%);
font-size:11px;color:rgba(255,255,255,.3);letter-spacing:.06em;z-index:5;
pointer-events:none;text-align:center;white-space:nowrap}
@media(max-width:700px){#tip{display:none}}
</style>
</head>
<body>
<div id="stage"><div id="wrap"><canvas id="cv" width="960" height="720"></canvas></div></div>
<div id="tip">← → 移動 / ↓ ソフト / SPACE ハードドロップ / ↑・X 右回転 / Z 左回転 / C・SHIFT ホールド / P ポーズ / R リスタート</div>
<script>
'use strict';
const cv=document.getElementById('cv'),ctx=cv.getContext('2d');
const W=960,H=720;
const COLS=10,ROWS=22,VIS_Y=2,CELL=28;
const BOARD_W=COLS*CELL,BOARD_H=20*CELL;
const BOARD_X=Math.round((W-BOARD_W)/2),BOARD_Y=Math.round((H-BOARD_H)/2)+8;
const COLORS={I:'#00e5e5',J:'#3a5cff',L:'#ff9b1a',O:'#ffd21a',S:'#3ae55c',T:'#c14bff',Z:'#ff3a4a'};
const GLOW={I:'rgba(0,229,229,.55)',J:'rgba(58,92,255,.55)',L:'rgba(255,155,26,.55)',
O:'rgba(255,210,26,.55)',S:'rgba(58,229,92,.55)',T:'rgba(193,75,255,.55)',Z:'rgba(255,58,74,.55)'};
const BASE={
I:[[0,0,0,0],[1,1,1,1],[0,0,0,0],[0,0,0,0]],
J:[[1,0,0],[1,1,1],[0,0,0]],
L:[[0,0,1],[1,1,1],[0,0,0]],
O:[[1,1],[1,1]],
S:[[0,1,1],[1,1,0],[0,0,0]],
T:[[0,1,0],[1,1,1],[0,0,0]],
Z:[[1,1,0],[0,1,1],[0,0,0]]};
const SPAWN={I:{x:3,y:1},J:{x:3,y:2},L:{x:3,y:2},O:{x:4,y:2},S:{x:3,y:2},T:{x:3,y:2},Z:{x:3,y:2}};
const KICKS_JLSTZ={
'01':[[0,0],[-1,0],[-1,-1],[0,2],[-1,2]],'10':[[0,0],[1,0],[1,1],[0,-2],[1,-2]],
'12':[[0,0],[1,0],[1,1],[0,-2],[1,-2]],'21':[[0,0],[-1,0],[-1,-1],[0,2],[-1,2]],
'23':[[0,0],[1,0],[1,-1],[0,2],[1,2]],'32':[[0,0],[-1,0],[-1,1],[0,-2],[-1,-2]],
'30':[[0,0],[-1,0],[-1,1],[0,-2],[-1,-2]],'03':[[0,0],[1,0],[1,-1],[0,2],[1,2]]};
const KICKS_I={
'01':[[0,0],[-2,0],[1,0],[-2,1],[1,-2]],'10':[[0,0],[2,0],[-1,0],[2,-1],[-1,2]],
'12':[[0,0],[-1,0],[2,0],[-1,-2],[2,1]],'21':[[0,0],[1,0],[-2,0],[1,2],[-2,-1]],
'23':[[0,0],[2,0],[-1,0],[2,-1],[-1,2]],'32':[[0,0],[-2,0],[1,0],[-2,1],[1,-2]],
'30':[[0,0],[1,0],[-2,0],[1,2],[-2,-1]],'03':[[0,0],[-1,0],[2,0],[-1,-2],[2,1]]};
const KICKS_O={'01':[[0,0]],'12':[[0,0]],'23':[[0,0]],'30':[[0,0]],
'10':[[0,0]],'21':[[0,0]],'32':[[0,0]],'03':[[0,0]]};
const T_FRONT={0:[[0,0],[2,0]],1:[[2,0],[2,2]],2:[[0,2],[2,2]],3:[[0,0],[0,2]]};
const AudioSys=(()=>{
let ac=null,master=null,sfxBus=null,bgmBus=null;
let bgmOn=true,sfxOn=true,bgmTimer=null,bgmStep=0,bgmIntensity=0;
function init(){
if(ac)return;
const AC=window.AudioContext||window.webkitAudioContext;
if(!AC)return;
ac=new AC();
master=ac.createGain();master.gain.value=0.85;master.connect(ac.destination);
sfxBus=ac.createGain();sfxBus.gain.value=0.9;sfxBus.connect(master);
const comp=ac.createDynamicsCompressor();
comp.threshold.value=-14;comp.knee.value=24;comp.ratio.value=8;
comp.attack.value=0.004;comp.release.value=0.18;comp.connect(master);
bgmBus=ac.createGain();bgmBus.gain.value=0.22;bgmBus.connect(comp);
}
function resume(){if(!ac)init();if(ac&&ac.state==='suspended')ac.resume();}
function env(g,t0,a,d,s,r,peak=1){
const gg=g.gain;gg.cancelScheduledValues(t0);gg.setValueAtTime(0.0001,t0);
gg.exponentialRampToValueAtTime(Math.max(peak,0.0001),t0+a);
gg.exponentialRampToValueAtTime(Math.max(s,0.0001),t0+a+d);
gg.setTargetAtTime(0.0001,t0+a+d,r);}
function tone({type='square',freq=440,freq2=null,dur=0.12,vol=0.3,
attack=0.005,decay=0.06,sustain=0.1,release=0.08,dest=null,delay=0}){
if(!ac||!sfxOn)return;
const t0=ac.currentTime+delay;
const o=ac.createOscillator(),g=ac.createGain();
o.type=type;o.frequency.setValueAtTime(freq,t0);
if(freq2)o.frequency.exponentialRampToValueAtTime(Math.max(freq2,1),t0+dur);
o.connect(g);g.connect(dest||sfxBus);
env(g,t0,attack,decay,sustain,release,vol);
o.start(t0);o.stop(t0+dur+release+0.05);}
function noise({dur=0.15,vol=0.25,filterType='lowpass',freq=800,q=1,dest=null,delay=0,sweepTo=null}){
if(!ac||!sfxOn)return;
const t0=ac.currentTime+delay;
const len=Math.max(1,Math.floor(ac.sampleRate*dur));
const buf=ac.createBuffer(1,len,ac.sampleRate);
const d=buf.getChannelData(0);
for(let i=0;i<len;i++)d[i]=Math.random()*2-1;
const src=ac.createBufferSource();src.buffer=buf;
const f=ac.createBiquadFilter();f.type=filterType;
f.frequency.setValueAtTime(freq,t0);f.Q.value=q;
if(sweepTo)f.frequency.exponentialRampToValueAtTime(Math.max(sweepTo,20),t0+dur);
const g=ac.createGain();src.connect(f);f.connect(g);g.connect(dest||sfxBus);
env(g,t0,0.004,dur*0.4,0.05,dur*0.5,vol);src.start(t0);}
const sfx={
move(){tone({type:'square',freq:180,freq2:140,dur:0.04,vol:0.12,attack:0.002,decay:0.02,sustain:0.02,release:0.03});},
rotate(){tone({type:'square',freq:320,freq2:520,dur:0.06,vol:0.16});
tone({type:'triangle',freq:640,freq2:900,dur:0.05,vol:0.1,delay:0.01});},
rotateFail(){tone({type:'sawtooth',freq:120,freq2:70,dur:0.08,vol:0.1});},
softDrop(){tone({type:'triangle',freq:220,dur:0.03,vol:0.06});},
hardDrop(){noise({dur:0.18,vol:0.35,filterType:'lowpass',freq:400,sweepTo:80});
tone({type:'sine',freq:90,freq2:40,dur:0.16,vol:0.4});},
lock(){tone({type:'triangle',freq:160,freq2:100,dur:0.06,vol:0.12});
noise({dur:0.05,vol:0.08,freq:600});},
hold(){tone({type:'sine',freq:520,freq2:380,dur:0.08,vol:0.18});
tone({type:'sine',freq:780,freq2:560,dur:0.08,vol:0.12,delay:0.04});},
clear(n){noise({dur:0.25+n*0.06,vol:0.3+n*0.05,filterType:'bandpass',freq:900,q:0.7,sweepTo:200});
const base=[523,659,784,988,1175];
for(let i=0;i<Math.min(n+1,5);i++){
tone({type:'square',freq:base[i],dur:0.1,vol:0.14,delay:i*0.045});
tone({type:'triangle',freq:base[i]*2,dur:0.08,vol:0.08,delay:i*0.045});}
if(n>=4){for(let i=0;i<6;i++)
tone({type:'sawtooth',freq:400+i*80,freq2:1200+i*100,dur:0.12,vol:0.08,delay:0.1+i*0.03});
noise({dur:0.5,vol:0.35,filterType:'highpass',freq:400,sweepTo:3000,delay:0.05});}},
tspin(mini){tone({type:'sawtooth',freq:220,freq2:880,dur:0.15,vol:0.2});
tone({type:'square',freq:mini?660:440,freq2:mini?990:1320,dur:0.18,vol:0.18,delay:0.05});
noise({dur:0.3,vol:0.15,filterType:'bandpass',freq:1500,q:2,delay:0.05});},
b2b(){[784,988,1175,1568].forEach((f,i)=>tone({type:'triangle',freq:f,dur:0.1,vol:0.12,delay:i*0.04}));},
combo(n){const f=300+n*40;tone({type:'square',freq:f,freq2:f*1.5,dur:0.07,vol:0.1});},
levelUp(){[262,330,392,523,659,784].forEach((f,i)=>{
tone({type:'square',freq:f,dur:0.1,vol:0.16,delay:i*0.05});
tone({type:'triangle',freq:f*2,dur:0.08,vol:0.08,delay:i*0.05});});},
phaseShift(){noise({dur:0.8,vol:0.3,filterType:'bandpass',freq:200,sweepTo:4000});
for(let i=0;i<8;i++)tone({type:'sawtooth',freq:100+i*60,freq2:800+i*100,dur:0.2,vol:0.1,delay:i*0.05});},
perfect(){[523,659,784,1047,1319,1568].forEach((f,i)=>{
tone({type:'square',freq:f,dur:0.14,vol:0.16,delay:i*0.06});
tone({type:'triangle',freq:f*1.5,dur:0.12,vol:0.1,delay:i*0.06});});},
gameOver(){[392,349,311,262,208,156].forEach((f,i)=>
tone({type:'sawtooth',freq:f,dur:0.25,vol:0.18,delay:i*0.14}));
noise({dur:1.2,vol:0.2,filterType:'lowpass',freq:500,sweepTo:60});},
start(){[262,330,392,523].forEach((f,i)=>tone({type:'square',freq:f,dur:0.09,vol:0.14,delay:i*0.06}));}};
const BASS=['A1',null,'A1',null,'A1',null,'A1','C2','D2',null,'D2',null,'D2',null,'D2','F2',
'A1',null,'A1',null,'A1',null,'A1','C2','G1',null,'G1','B1','D2',null,'F2',null];
const LEAD1=['A4',null,'C5','D5','E5',null,'D5',null,'C5',null,'A4',null,'G4',null,'A4',null,
'F4',null,'A4','C5','D5',null,'C5',null,'B4',null,'G4',null,'A4',null,null,null];
const LEAD2=['E5',null,'G5','A5','B5',null,'A5',null,'G5',null,'E5',null,'D5',null,'E5',null,
'C5',null,'E5','G5','A5',null,'G5',null,'F5',null,'D5',null,'E5',null,null,null];
const ARP=['A3','C4','E4','A4','C4','E4','A4','E4','D3','F3','A3','D4','F3','A3','D4','A3',
'A3','C4','E4','A4','C4','E4','A4','E4','G3','B3','D4','G4','B3','D4','G4','D4'];
const NOTE=(()=>{const names=['C','C#','D','D#','E','F','F#','G','G#','A','A#','B'];const m={};
for(let o=0;o<=8;o++)for(let n=0;n<12;n++)m[names[n]+o]=440*Math.pow(2,(n-9)/12+(o-4));return m;})();
function playNote(name,{type='square',dur=0.1,vol=0.2,dest=bgmBus,delay=0}){
if(!ac||!bgmOn||!name)return;
const freq=NOTE[name];if(!freq)return;
const t0=ac.currentTime+delay;
const o=ac.createOscillator(),g=ac.createGain();
o.type=type;o.frequency.value=freq;o.connect(g);g.connect(dest);
g.gain.setValueAtTime(0.0001,t0);g.gain.exponentialRampToValueAtTime(vol,t0+0.008);
g.gain.exponentialRampToValueAtTime(0.0001,t0+dur);o.start(t0);o.stop(t0+dur+0.03);}
function playNoiseHit({dur=0.04,vol=0.12,hp=2000}){
if(!ac||!bgmOn)return;
const t0=ac.currentTime;const len=Math.max(1,Math.floor(ac.sampleRate*dur));
const buf=ac.createBuffer(1,len,ac.sampleRate);const d=buf.getChannelData(0);
for(let i=0;i<len;i++)d[i]=Math.random()*2-1;
const src=ac.createBufferSource();src.buffer=buf;
const f=ac.createBiquadFilter();f.type='highpass';f.frequency.value=hp;
const g=ac.createGain();src.connect(f);f.connect(g);g.connect(bgmBus);
g.gain.setValueAtTime(vol,t0);g.gain.exponentialRampToValueAtTime(0.0001,t0+dur);src.start(t0);}
function bgmTick(){
if(!ac||!bgmOn)return;
const s=bgmStep%32;const inten=bgmIntensity;
playNote(BASS[s],{type:'triangle',dur:0.12,vol:0.35});
if(inten>=1)playNote(LEAD1[s],{type:'square',dur:0.1,vol:0.12});
if(inten>=2)playNote(LEAD2[s],{type:'square',dur:0.08,vol:0.07});
if(inten>=1&&s%2===0)playNote(ARP[s],{type:'sawtooth',dur:0.05,vol:0.04});
if(s%2===1)playNoiseHit({dur:0.03,vol:0.06,hp:6000});
if(s%8===0)playNoiseHit({dur:0.08,vol:0.1,hp:1500});
if(s%8===4)playNoiseHit({dur:0.05,vol:0.08,hp:2500});
bgmStep++;}
function startBGM(){if(bgmTimer)return;bgmStep=0;bgmTimer=setInterval(bgmTick,125);}
function stopBGM(){if(bgmTimer){clearInterval(bgmTimer);bgmTimer=null;}}
return{init,resume,sfx,startBGM,stopBGM,
setIntensity(v){bgmIntensity=Math.max(0,Math.min(3,v|0));},
toggleBGM(){bgmOn=!bgmOn;if(!bgmOn)stopBGM();return bgmOn;},
toggleSFX(){sfxOn=!sfxOn;return sfxOn;}};
})();
const EFFECT_NAMES=['BURST','SHOCKWAVE','LASER','STARDUST','INFERNO','ICE',
'THUNDER','COSMIC','METEOR','MATRIX','GEOMETRY','RAVE'];
const FX=(()=>{
const particles=[],rings=[],lasers=[],bolts=[],texts=[],trails=[],shapeBursts=[];
let shakeT=0,shakeMag=0,flashA=0,flashCol='255,255,255',chroma=0,vignette=0,phaseGlow=0;
let rainCols=[];
function reset(){
particles.length=0;rings.length=0;lasers.length=0;bolts.length=0;
texts.length=0;trails.length=0;shapeBursts.length=0;
shakeT=0;shakeMag=0;flashA=0;chroma=0;vignette=0;phaseGlow=0;rainCols=[];}
function shake(mag,t=0.35){shakeMag=Math.max(shakeMag,mag);shakeT=Math.max(shakeT,t);}
function flash(r,g,b,a=0.5){flashCol=r+','+g+','+b;flashA=Math.min(1,flashA+a);}
function chromaBurst(v=0.5){chroma=Math.min(1.2,chroma+v);}
function addText(x,y,str,opts={}){
texts.push({x,y,str,t:0,life:opts.life||1,size:opts.size||28,
color:opts.color||'#fff',glow:opts.glow||'#fff',
vy:opts.vy!=null?opts.vy:-40,scale:opts.scale||1});}
function spawnParticle(x,y,opt){
particles.push({x,y,vx:opt.vx||0,vy:opt.vy||0,g:opt.g!=null?opt.g:300,
life:opt.life||1,t:0,size:opt.size||3,col:opt.col||'#fff',
shape:opt.shape||'circle',rot:opt.rot||0,vr:opt.vr||0,
trail:opt.trail||false,drag:opt.drag!=null?opt.drag:0.02});}
function burst(x,y,col,n=24,power=260,opts={}){
for(let i=0;i<n;i++){
const a=Math.random()*Math.PI*2;
const sp=power*(0.35+Math.random()*0.9);
spawnParticle(x,y,{
vx:Math.cos(a)*sp,vy:Math.sin(a)*sp-(opts.lift||40),
g:opts.g!=null?opts.g:420,life:0.5+Math.random()*0.7,
size:(opts.size||3)*(0.6+Math.random()*1.2),
col:Array.isArray(col)?col[i%col.length]:col,
shape:opts.shape||(Math.random()<0.3?'spark':'circle'),
rot:Math.random()*6.28,vr:(Math.random()-0.5)*12,trail:Math.random()<0.35});}}
function rowExplosion(rowY,intensity=1){
const cy=BOARD_Y+(rowY-VIS_Y+0.5)*CELL;
for(let c=0;c<COLS;c++){
const x=BOARD_X+(c+0.5)*CELL;
burst(x,cy,['#fff','#ffe66d','#ff6b6b','#7cf'],8+intensity*3,200+intensity*60,{shape:'spark'});}}
function typeBurst(rowYs,level){
const cy=BOARD_Y+(rowYs[0]-VIS_Y+rowYs.length/2)*CELL;
const cx=BOARD_X+BOARD_W/2;
burst(cx,cy,['#fff','#ffe66d','#ff9f1a','#ff3355','#7cf'],80+level*8,420+level*30,{shape:'spark'});
rings.push({x:cx,y:cy,r:10,vr:600,life:0.5,t:0,col:'#ffe66d',w:6});
rings.push({x:cx,y:cy,r:5,vr:380,life:0.7,t:0,col:'#fff',w:3});
shake(10+level*1.5,0.4);flash(255,220,120,0.45+level*0.02);chromaBurst(0.5);}
function typeShockwave(rowYs,level){
const cy=BOARD_Y+(rowYs[0]-VIS_Y+rowYs.length/2)*CELL;
const cx=BOARD_X+BOARD_W/2;
for(let i=0;i<4+level;i++)
rings.push({x:cx,y:cy,r:i*14,vr:420+i*60+level*30,life:0.7+i*0.05,t:0,
col:'hsl('+((i*40+level*25)%360)+',100%,65%)',w:8-i*0.5});
for(let i=0;i<50;i++){
const a=(i/50)*Math.PI*2;
spawnParticle(cx,cy,{vx:Math.cos(a)*(380+level*40),vy:Math.sin(a)*(380+level*40),
g:0,life:0.6,size:4,col:'hsl('+((i*7)%360)+',100%,70%)',shape:'spark',trail:true});}
shake(12+level*2,0.45);flash(120,220,255,0.4);}
function typeLaser(rowYs,level){
const cy=BOARD_Y+(rowYs[0]-VIS_Y+rowYs.length/2)*CELL;
const cx=BOARD_X+BOARD_W/2;
for(let i=0;i<rowYs.length;i++){
const y=BOARD_Y+(rowYs[i]-VIS_Y+0.5)*CELL;
lasers.push({x:-20,y,w:W+40,h:6+level,col:'#ff2d55',t:0,life:0.35,hor:true});}
for(let i=0;i<6+level;i++){
const x=BOARD_X+Math.random()*BOARD_W;
lasers.push({x,y:-20,w:5+level,h:H+40,col:i%2?'#0ff':'#ff2d55',t:0,life:0.3,hor:false});}
burst(cx,cy,['#0ff','#ff2d55','#fff'],40,300);
shake(14+level*2,0.4);flash(255,80,120,0.5);chromaBurst(0.7);}
function typeStardust(rowYs,level){
const cy=BOARD_Y+(rowYs[0]-VIS_Y+rowYs.length/2)*CELL;
const cx=BOARD_X+BOARD_W/2;
for(let i=0;i<100+level*10;i++){
const x=BOARD_X+Math.random()*BOARD_W;
const y=BOARD_Y+Math.random()*BOARD_H;
spawnParticle(x,y,{vx:(Math.random()-0.5)*120,vy:-80-Math.random()*200,
g:-30,life:1.2+Math.random(),size:1.5+Math.random()*3,
col:'hsl('+(50+Math.random()*60)+',100%,'+(70+Math.random()*30)+'%)',
shape:'star',rot:Math.random()*6.28,vr:(Math.random()-0.5)*8,trail:true});}
rings.push({x:cx,y:cy,r:20,vr:300,life:0.8,t:0,col:'#fff6c8',w:2});
shake(8+level,0.5);flash(255,246,200,0.35);}
function typeInferno(rowYs,level){
for(const ry of rowYs){
const y=BOARD_Y+(ry-VIS_Y+1)*CELL;
for(let i=0;i<40+level*5;i++){
const x=BOARD_X+Math.random()*BOARD_W;
spawnParticle(x,y+Math.random()*20,{
vx:(Math.random()-0.5)*80,vy:-180-Math.random()*320,g:-200,
life:0.6+Math.random()*0.6,size:4+Math.random()*8,
col:['#ff3300','#ff6a00','#ffcc00','#fff0a0'][i%4],shape:'circle'});}}
shake(12+level*1.5,0.4);flash(255,120,40,0.4);}
function typeIce(rowYs,level){
const cy=BOARD_Y+(rowYs[0]-VIS_Y+rowYs.length/2)*CELL;
const cx=BOARD_X+BOARD_W/2;
for(let i=0;i<60+level*8;i++){
const x=BOARD_X+Math.random()*BOARD_W;
spawnParticle(x,cy+(Math.random()-0.5)*40,{
vx:(Math.random()-0.5)*400,vy:(Math.random()-0.5)*400,g:250,
life:0.8+Math.random()*0.5,size:3+Math.random()*6,
col:['#b8f3ff','#7ad7ff','#e8ffff','#4bc7ff'][i%4],
shape:'shard',rot:Math.random()*6.28,vr:(Math.random()-0.5)*16});}
rings.push({x:cx,y:cy,r:15,vr:500,life:0.5,t:0,col:'#b8f3ff',w:4});
shake(10+level,0.35);flash(180,240,255,0.5);}
function typeThunder(rowYs,level){
const cx=BOARD_X+BOARD_W/2,cy=BOARD_Y+BOARD_H/2;
for(let i=0;i<4+level;i++){
const x0=BOARD_X+Math.random()*BOARD_W;
bolts.push({x0,y0:-30,x1:x0+(Math.random()-0.5)*120,
y1:BOARD_Y+BOARD_H*(0.3+Math.random()*0.7),t:0,life:0.25+Math.random()*0.15,w:3+level*0.5});}
for(const ry of rowYs){
const y=BOARD_Y+(ry-VIS_Y+0.5)*CELL;
lasers.push({x:BOARD_X,y,w:BOARD_W,h:8,col:'#e0f0ff',t:0,life:0.2,hor:true});}
burst(cx,cy,['#fff','#cde7ff','#7ab8ff'],50,350);
shake(16+level*2,0.4);flash(220,240,255,0.7);}
function typeCosmic(rowYs,level){
const cx=BOARD_X+BOARD_W/2,cy=BOARD_Y+BOARD_H/2;
for(let i=0;i<80+level*10;i++){
const a=Math.random()*Math.PI*2,r=20+Math.random()*80;
const sp=250+Math.random()*250+level*20;
spawnParticle(cx+Math.cos(a)*r,cy+Math.sin(a)*r,{
vx:Math.cos(a)*sp,vy:Math.sin(a)*sp,g:-80,life:1+Math.random(),
size:2+Math.random()*3,col:'hsl('+((i*3+level*20)%360)+',100%,70%)',
shape:'star',trail:true});}
rings.push({x:cx,y:cy,r:5,vr:550,life:0.9,t:0,col:'#d08cff',w:5});
rings.push({x:cx,y:cy,r:0,vr:400,life:1.1,t:0,col:'#8cd0ff',w:3});
shake(12+level*1.5,0.5);flash(180,100,255,0.45);chromaBurst(0.8);}
function typeMeteor(rowYs,level){
for(let i=0;i<10+level;i++){
const x=BOARD_X+Math.random()*BOARD_W,y=-30-Math.random()*200;
trails.push({x,y,vx:30+Math.random()*40,vy:500+Math.random()*400,t:0,life:1.2,col:'#ffb347',size:5});}
for(const ry of rowYs){
const y=BOARD_Y+(ry-VIS_Y+0.5)*CELL;
burst(BOARD_X+BOARD_W/2,y,['#ffb347','#ff6a00','#fff0a0'],30,280);}
shake(13+level*1.5,0.45);flash(255,170,70,0.45);}
function typeMatrix(rowYs,level){
if(!rainCols.length)
for(let i=0;i<40;i++)rainCols.push({x:Math.random()*W,y:Math.random()*H,sp:200+Math.random()*400});
for(const c of rainCols)
for(let i=0;i<3;i++)
spawnParticle(c.x,c.y+i*10,{vx:0,vy:200+Math.random()*200,g:0,life:0.5,
size:2,col:'#39ff6a',shape:'square'});
for(const ry of rowYs){
const y=BOARD_Y+(ry-VIS_Y+0.5)*CELL;
lasers.push({x:0,y,w:W,h:3,col:'#39ff6a',t:0,life:0.4,hor:true});}
shake(9+level,0.35);flash(40,255,100,0.3);}
function typeGeometry(rowYs,level){
const cx=BOARD_X+BOARD_W/2,cy=BOARD_Y+BOARD_H/2;
for(let i=0;i<12+level;i++)
shapeBursts.push({x:cx,y:cy,r:10,vr:200+i*30,t:0,life:0.9,
sides:3+(i%5),rot:Math.random()*6.28,vr2:(Math.random()-0.5)*4,
col:'hsl('+((i*30)%360)+',100%,60%)',w:3});
burst(cx,cy,['#0ff','#f0f','#ff0','#fff'],50,320,{shape:'square'});
shake(11+level*1.2,0.4);flash(255,50,255,0.4);chromaBurst(0.6);}
function typeRave(rowYs,level){
const cx=BOARD_X+BOARD_W/2,cy=BOARD_Y+BOARD_H/2;
for(let i=0;i<6;i++)
rings.push({x:cx,y:cy,r:i*20,vr:480+i*40,life:0.6,t:0,
col:'hsl('+((i*60)%360)+',100%,60%)',w:10});
for(let i=0;i<120+level*10;i++){
const a=Math.random()*Math.PI*2;
spawnParticle(cx,cy,{
vx:Math.cos(a)*(200+Math.random()*500),vy:Math.sin(a)*(200+Math.random()*500),
g:100,life:0.8,size:3+Math.random()*4,
col:'hsl('+(Math.random()*360)+',100%,65%)',
shape:Math.random()<0.5?'square':'star',rot:Math.random()*6.28,vr:(Math.random()-0.5)*20});}
shake(15+level*2,0.5);flash(255,255,255,0.6);chromaBurst(1);}
const TYPES=[typeBurst,typeShockwave,typeLaser,typeStardust,typeInferno,typeIce,
typeThunder,typeCosmic,typeMeteor,typeMatrix,typeGeometry,typeRave];
function trigger(rowYs,effectIndex,level,linesCleared){
TYPES[effectIndex%TYPES.length](rowYs,level,linesCleared);
rowExplosion(rowYs[0],1+linesCleared*0.3);
if(linesCleared>=2)rowExplosion(rowYs[rowYs.length-1],1);}
function phaseChange(effectIndex,level){
const cx=W/2,cy=H/2;
flash(255,255,255,0.85);shake(22+level*2,0.7);chromaBurst(1);vignette=1;phaseGlow=1;
for(let i=0;i<8;i++)
rings.push({x:cx,y:cy,r:i*30,vr:700+i*80,life:1+i*0.05,t:0,
col:'hsl('+((i*45+effectIndex*20)%360)+',100%,65%)',w:12-i});
for(let i=0;i<200;i++){
const a=Math.random()*Math.PI*2,sp=100+Math.random()*700;
spawnParticle(cx,cy,{vx:Math.cos(a)*sp,vy:Math.sin(a)*sp,g:200,life:1+Math.random(),
size:3+Math.random()*5,col:'hsl('+(Math.random()*360)+',100%,65%)',
shape:['spark','star','square','shard'][i%4],rot:Math.random()*6.28,
vr:(Math.random()-0.5)*16,trail:true});}
addText(cx,120,'EFFECT SHIFT!',{size:40,color:'#fff',glow:'#f0f',life:1.4,vy:-10,scale:1.05});
addText(cx,175,EFFECT_NAMES[effectIndex%EFFECT_NAMES.length],
{size:28,color:'#ffe66d',glow:'#f90',life:1.4,vy:-8,scale:1});
addText(cx,220,'LEVEL '+(level+1),{size:22,color:'#cdefff',glow:'#8cf',life:1.2,vy:-6});}
function update(dt){
for(let i=particles.length-1;i>=0;i--){
const p=particles[i];p.t+=dt;
if(p.t>=p.life){particles.splice(i,1);continue;}
p.vy+=p.g*dt;p.vx*=(1-p.drag);p.vy*=(1-p.drag*0.5);
p.x+=p.vx*dt;p.y+=p.vy*dt;p.rot+=p.vr*dt;}
for(let i=rings.length-1;i>=0;i--){
const r=rings[i];r.t+=dt;
if(r.t>=r.life){rings.splice(i,1);continue;}r.r+=r.vr*dt;}
for(let i=lasers.length-1;i>=0;i--){const l=lasers[i];l.t+=dt;if(l.t>=l.life)lasers.splice(i,1);}
for(let i=bolts.length-1;i>=0;i--){const b=bolts[i];b.t+=dt;if(b.t>=b.life)bolts.splice(i,1);}
for(let i=trails.length-1;i>=0;i--){
const t=trails[i];t.t+=dt;
if(t.t>=t.life){trails.splice(i,1);continue;}t.x+=t.vx*dt;t.y+=t.vy*dt;}
for(let i=texts.length-1;i>=0;i--){
const t=texts[i];t.t+=dt;
if(t.t>=t.life){texts.splice(i,1);continue;}t.y+=t.vy*dt;}
for(let i=shapeBursts.length-1;i>=0;i--){
const s=shapeBursts[i];s.t+=dt;
if(s.t>=s.life){shapeBursts.splice(i,1);continue;}s.r+=s.vr*dt;s.rot+=s.vr2*dt;}
for(const c of rainCols){c.y+=c.sp*dt;if(c.y>H+40){c.y=-20;c.x=Math.random()*W;}}
if(shakeT>0)shakeT=Math.max(0,shakeT-dt);else shakeMag*=0.85;
if(flashA>0)flashA=Math.max(0,flashA-dt*2.2);
if(chroma>0)chroma=Math.max(0,chroma-dt*1.5);
if(vignette>0)vignette=Math.max(0,vignette-dt*1.2);
if(phaseGlow>0)phaseGlow=Math.max(0,phaseGlow-dt*0.6);}
function draw(){
for(const p of particles){
const a=1-p.t/p.life;ctx.save();ctx.globalAlpha=a;
if(p.shape!=='square')ctx.shadowColor=p.col,ctx.shadowBlur=12;
ctx.fillStyle=p.col;ctx.translate(p.x,p.y);ctx.rotate(p.rot);
const s=p.size*(0.4+0.6*a);
if(p.shape==='circle'){ctx.beginPath();ctx.arc(0,0,s,0,6.28);ctx.fill();}
else if(p.shape==='square')ctx.fillRect(-s,-s,s*2,s*2);
else if(p.shape==='spark'){ctx.fillRect(-s*2,-s*0.4,s*4,s*0.8);ctx.fillRect(-s*0.4,-s*2,s*0.8,s*4);}
else if(p.shape==='star'){ctx.beginPath();
for(let i=0;i<5;i++){const a1=(i*4*Math.PI)/5,a2=a1+(2*Math.PI)/5,r1=s*1.6,r2=s*0.7;
if(i===0)ctx.moveTo(Math.cos(a1)*r1,Math.sin(a1)*r1);else ctx.lineTo(Math.cos(a1)*r1,Math.sin(a1)*r1);
ctx.lineTo(Math.cos(a2)*r2,Math.sin(a2)*r2);}
ctx.closePath();ctx.fill();}
else if(p.shape==='shard'){ctx.beginPath();ctx.moveTo(0,-s*1.5);ctx.lineTo(s*0.8,0);
ctx.lineTo(0,s*1.5);ctx.lineTo(-s*0.8,0);ctx.closePath();ctx.fill();}
if(p.trail){ctx.globalAlpha=a*0.4;ctx.beginPath();ctx.moveTo(0,0);
ctx.lineTo(-p.vx*0.03,-p.vy*0.03);ctx.strokeStyle=p.col;ctx.lineWidth=p.size*0.8;ctx.stroke();}
ctx.restore();}
for(const r of rings){const a=1-r.t/r.life;ctx.save();ctx.globalAlpha=a*0.9;
ctx.strokeStyle=r.col;ctx.lineWidth=Math.max(0.5,r.w*a);ctx.shadowColor=r.col;ctx.shadowBlur=20;
ctx.beginPath();ctx.arc(r.x,r.y,r.r,0,6.28);ctx.stroke();ctx.restore();}
for(const l of lasers){const a=1-l.t/l.life;ctx.save();ctx.globalAlpha=a;ctx.fillStyle=l.col;
ctx.shadowColor=l.col;ctx.shadowBlur=25;
if(l.hor)ctx.fillRect(l.x,l.y-l.h/2,l.w,l.h);else ctx.fillRect(l.x-l.w/2,l.y,l.w,l.h);
ctx.globalAlpha=a*0.5;
if(l.hor)ctx.fillRect(l.x,l.y-l.h,l.w,l.h*2);else ctx.fillRect(l.x-l.w,l.y,l.w*2,l.h);
ctx.restore();}
for(const b of bolts){const a=1-b.t/b.life;ctx.save();ctx.globalAlpha=a;
ctx.strokeStyle='#eaf6ff';ctx.lineWidth=b.w;ctx.shadowColor='#8cf';ctx.shadowBlur=30;
ctx.beginPath();ctx.moveTo(b.x0,b.y0);const seg=8;
for(let i=1;i<seg;i++){const t=i/seg,jx=(Math.random()-0.5)*40;
ctx.lineTo(b.x0+(b.x1-b.x0)*t+jx,b.y0+(b.y1-b.y0)*t);}
ctx.lineTo(b.x1,b.y1);ctx.stroke();ctx.restore();}
for(const t of trails){const a=1-t.t/t.life;ctx.save();ctx.globalAlpha=a;
const grad=ctx.createLinearGradient(t.x,t.y,t.x-t.vx*0.15,t.y-t.vy*0.15);
grad.addColorStop(0,t.col);grad.addColorStop(1,'transparent');
ctx.strokeStyle=grad;ctx.lineWidth=t.size;ctx.shadowColor=t.col;ctx.shadowBlur=16;
ctx.beginPath();ctx.moveTo(t.x,t.y);ctx.lineTo(t.x-t.vx*0.15,t.y-t.vy*0.15);ctx.stroke();
ctx.fillStyle='#fff';ctx.beginPath();ctx.arc(t.x,t.y,t.size*0.6,0,6.28);ctx.fill();ctx.restore();}
for(const s of shapeBursts){const a=1-s.t/s.life;ctx.save();ctx.globalAlpha=a*0.8;
ctx.strokeStyle=s.col;ctx.lineWidth=s.w;ctx.shadowColor=s.col;ctx.shadowBlur=16;
ctx.translate(s.x,s.y);ctx.rotate(s.rot);ctx.beginPath();
for(let i=0;i<s.sides;i++){const ang=(i/s.sides)*Math.PI*2;
const px=Math.cos(ang)*s.r,py=Math.sin(ang)*s.r;
if(i===0)ctx.moveTo(px,py);else ctx.lineTo(px,py);}
ctx.closePath();ctx.stroke();ctx.restore();}
for(const t of texts){const a=1-t.t/t.life;
const pop=t.t<0.15?0.7+(t.t/0.15)*0.5:1.2-(t.t/t.life)*0.1;
ctx.save();ctx.globalAlpha=Math.min(1,a*1.4);ctx.translate(t.x,t.y);
ctx.scale(pop*t.scale,pop*t.scale);
ctx.font='900 '+t.size+'px "Segoe UI",system-ui,sans-serif';
ctx.textAlign='center';ctx.textBaseline='middle';
ctx.shadowColor=t.glow;ctx.shadowBlur=22;ctx.fillStyle=t.color;ctx.fillText(t.str,0,0);
ctx.shadowBlur=0;ctx.lineWidth=2;ctx.strokeStyle='rgba(0,0,0,.5)';ctx.strokeText(t.str,0,0);
ctx.restore();}}
function drawOverlay(){
if(flashA>0){ctx.fillStyle='rgba('+flashCol+','+(flashA*0.38)+')';ctx.fillRect(0,0,W,H);}
if(vignette>0){const g=ctx.createRadialGradient(W/2,H/2,H*0.2,W/2,H/2,H*0.7);
g.addColorStop(0,'transparent');g.addColorStop(1,'rgba(255,255,255,'+(vignette*0.15)+')');
ctx.fillStyle=g;ctx.fillRect(0,0,W,H);}}
function getShake(){
if(shakeT<=0&&shakeMag<0.2)return{x:0,y:0};
const m=shakeMag;return{x:(Math.random()-0.5)*m*2,y:(Math.random()-0.5)*m*2};}
return{reset,shake,flash,chromaBurst,addText,burst,trigger,phaseChange,
update,draw,drawOverlay,getShake,getChroma:()=>chroma,getPhaseGlow:()=>phaseGlow};
})();
function emptyBoard(){return Array.from({length:ROWS},()=>Array(COLS).fill(null));}
function rotateMatrix(m,cw=true){
const n=m.length,r=Array.from({length:n},()=>Array(n).fill(0));
for(let y=0;y<n;y++)for(let x=0;x<n;x++){
if(cw)r[x][n-1-y]=m[y][x];else r[n-1-x][y]=m[y][x];}
return r;}
function allRotations(base){
const rots=[base];for(let i=1;i<4;i++)rots.push(rotateMatrix(rots[i-1],true));return rots;}
const ROT_CACHE={};
for(const k of Object.keys(BASE))ROT_CACHE[k]=allRotations(BASE[k]);
const Game={
board:emptyBoard(),queue:[],bag:[],cur:null,hold:null,canHold:true,
score:0,lines:0,level:1,combo:-1,b2b:-1,started:false,paused:false,over:false,
dropTimer:0,lockTimer:0,locking:false,lockResets:0,lastKick:0,lastActionRotate:false,
softDrop:false,dasTimer:0,arrTimer:0,moveDir:0,pieceCount:0,effectIndex:0,effectLevel:0,
message:null,startTime:0,
stats:{singles:0,doubles:0,triples:0,tetris:0,tspin:0,tspimini:0,perfect:0,pieces:0},
keys:{},prevKeys:{}};
const DAS_MS=150,ARR_MS=33,LOCK_MS=500,MAX_LOCK_RESETS=15,SOFT_DROP_FACTOR=20;
function refillBag(){
const bag=['I','J','L','O','S','T','Z'];
for(let i=bag.length-1;i>0;i--){const j=(Math.random()*(i+1))|0;const t=bag[i];bag[i]=bag[j];bag[j]=t;}
Game.bag.push(...bag);}
function nextType(){if(Game.bag.length<7)refillBag();return Game.bag.shift();}
function fillQueue(){while(Game.queue.length<6)Game.queue.push(nextType());}
function makePiece(type){
const s=SPAWN[type];return{type,rot:0,x:s.x,y:s.y,matrix:ROT_CACHE[type][0]};}
function cellsOf(piece){
const cells=[],m=piece.matrix;
for(let y=0;y<m.length;y++)for(let x=0;x<m[y].length;x++)
if(m[y][x])cells.push([piece.x+x,piece.y+y]);
return cells;}
function collides(piece,ox=0,oy=0,matrix=null){
const m=matrix||piece.matrix;
for(let y=0;y<m.length;y++)for(let x=0;x<m[y].length;x++){
if(!m[y][x])continue;
const bx=piece.x+x+ox,by=piece.y+y+oy;
if(bx<0||bx>=COLS||by>=ROWS)return true;
if(by<0)continue;
if(Game.board[by][bx])return true;}
return false;}
function spawnPiece(type){
if(!type){fillQueue();type=Game.queue.shift();fillQueue();}
Game.cur=makePiece(type);
Game.canHold=true;Game.locking=false;Game.lockTimer=0;Game.lockResets=0;
Game.lastActionRotate=false;Game.lastKick=0;Game.dropTimer=0;
Game.pieceCount++;Game.stats.pieces++;
if(collides(Game.cur)){
Game.cur.y-=1;
if(collides(Game.cur)){Game.cur.y+=1;gameOver();}}}
function gravityMs(){
const lv=Math.min(Game.level,20);
const sec=Math.pow(0.8-(lv-1)*0.007,lv-1);
return Math.max(16,sec*1000);}
function tryMove(dx,dy){
if(!Game.cur||Game.over||Game.paused)return false;
if(collides(Game.cur,dx,dy))return false;
Game.cur.x+=dx;Game.cur.y+=dy;
if(dx!==0){Game.lastActionRotate=false;resetLockIfNeeded();}
return true;}
function resetLockIfNeeded(){
if(Game.locking&&Game.lockResets<MAX_LOCK_RESETS){Game.lockTimer=0;Game.lockResets++;}}
function getKicks(type,from,to){
const key=''+from+to;
if(type==='I')return KICKS_I[key]||[[0,0]];
if(type==='O')return KICKS_O[key]||[[0,0]];
return KICKS_JLSTZ[key]||[[0,0]];}
function tryRotate(cw){
if(!Game.cur||Game.over||Game.paused)return false;
const type=Game.cur.type;
if(type==='O')return true;
const from=Game.cur.rot;
const to=cw?(from+1)%4:(from+3)%4;
const newMatrix=ROT_CACHE[type][to];
const kicks=getKicks(type,from,to);
for(let i=0;i<kicks.length;i++){
const kx=kicks[i][0],ky=kicks[i][1];
if(!collides(Game.cur,kx,ky,newMatrix)){
Game.cur.x+=kx;Game.cur.y+=ky;Game.cur.rot=to;Game.cur.matrix=newMatrix;
Game.lastActionRotate=true;Game.lastKick=i;
resetLockIfNeeded();AudioSys.sfx.rotate();return true;}}
AudioSys.sfx.rotateFail();return false;}
function ghostY(){
if(!Game.cur)return 0;
let dy=0;while(!collides(Game.cur,0,dy+1))dy++;
return Game.cur.y+dy;}
function detectTSpin(){
if(!Game.cur||Game.cur.type!=='T'||!Game.lastActionRotate)return null;
const ox=Game.cur.x,oy=Game.cur.y;
const corners=[[ox,oy],[ox+2,oy],[ox,oy+2],[ox+2,oy+2]];
let filled=0;
for(const c of corners){
const cx=c[0],cy=c[1];
if(cx<0||cx>=COLS||cy>=ROWS){filled++;continue;}
if(cy<0)continue;
if(Game.board[cy][cx])filled++;}
if(filled<3)return null;
if(Game.lastKick===4)return{mini:false};
const fronts=T_FRONT[Game.cur.rot];
let frontFilled=0;
for(const f of fronts){
const cx=ox+f[0],cy=oy+f[1];
if(cx<0||cx>=COLS||cy>=ROWS){frontFilled++;continue;}
if(cy<0)continue;
if(Game.board[cy][cx])frontFilled++;}
return{mini:frontFilled<2};}
function hardDrop(){
if(!Game.cur||Game.over||Game.paused)return;
let d=0;while(!collides(Game.cur,0,d+1))d++;
Game.cur.y+=d;Game.score+=d*2;
AudioSys.sfx.hardDrop();
FX.shake(4,0.12);
const cells=cellsOf(Game.cur);
for(const c of cells)
FX.burst(BOARD_X+(c[0]+0.5)*CELL,BOARD_Y+(c[1]-VIS_Y+1)*CELL,
['#ffffffaa','#cccccc88'],4,80,{shape:'circle',g:200});
lockPiece(true);}
function isPerfectClear(){
for(let y=0;y<ROWS;y++)for(let x=0;x<COLS;x++)if(Game.board[y][x])return false;
return true;}
function lockPiece(fromHard=false){
if(!Game.cur)return;
const tspin=detectTSpin();
const cells=cellsOf(Game.cur);
let anyVisible=false;
for(const c of cells){
const x=c[0],y=c[1];
if(y>=0&&y<ROWS&&x>=0&&x<COLS){Game.board[y][x]=Game.cur.type;if(y>=VIS_Y)anyVisible=true;}}
if(!fromHard)AudioSys.sfx.lock();
const full=[];
for(let y=0;y<ROWS;y++)if(Game.board[y].every(function(c){return c;}))full.push(y);
if(full.length===0&&!tspin){
Game.combo=-1;
if(!anyVisible){gameOver();return;}
spawnPiece();return;}
const n=full.length;
let base=0,clearType='',diff=false;
if(tspin){
if(tspin.mini){
Game.stats.tspimini++;
if(n===0){base=100;clearType='T-SPIN MINI';}
else if(n===1){base=200;clearType='T-SPIN MINI';diff=true;}
else if(n===2){base=400;clearType='T-SPIN MINI';diff=true;}}
else{
Game.stats.tspin++;
if(n===0){base=400;clearType='T-SPIN';}
else if(n===1){base=800;clearType='T-SPIN';diff=true;Game.stats.singles++;}
else if(n===2){base=1200;clearType='T-SPIN';diff=true;Game.stats.doubles++;}
else{base=1600;clearType='T-SPIN';diff=true;Game.stats.triples++;}}
AudioSys.sfx.tspin(!!tspin.mini);}
else if(n>0){
if(n===1){base=100;clearType='SINGLE';Game.stats.singles++;}
else if(n===2){base=300;clearType='DOUBLE';Game.stats.doubles++;}
else if(n===3){base=500;clearType='TRIPLE';Game.stats.triples++;}
else{base=800;clearType='TETRIS';diff=true;Game.stats.tetris++;}
AudioSys.sfx.clear(n);}
if(n>0){
Game.combo++;
if(Game.combo>0){base+=50*Game.combo;AudioSys.sfx.combo(Game.combo);}}
if(diff&&n>0){
if(Game.b2b>0){base=Math.floor(base*1.5);AudioSys.sfx.b2b();}
Game.b2b=1;}
else if(n>0)Game.b2b=0;
base*=Game.level;
Game.score+=base;
const clearedYs=full.slice();
if(n>0){
Game.board=Game.board.filter(function(row,y){return full.indexOf(y)<0;});
while(Game.board.length<ROWS)Game.board.unshift(Array(COLS).fill(null));
Game.lines+=n;
const newLevel=Math.floor(Game.lines/10)+1;
const oldLevel=Game.level;
Game.level=newLevel;
if(Game.level>oldLevel){
AudioSys.sfx.levelUp();}}
const newEffect=Math.floor(Game.lines/10);
const oldEffect=Game.effectIndex;
Game.effectIndex=newEffect;Game.effectLevel=newEffect;
if(newEffect>oldEffect&&n>0){
FX.phaseChange(newEffect,newEffect);
AudioSys.sfx.phaseShift();}
AudioSys.setIntensity(Math.min(3,Math.floor(Game.lines/20)));
let perfect=false;
if(n>0&&isPerfectClear()){
perfect=true;Game.stats.perfect++;
Game.score+=[0,800,1200,1800,2000][n]*Game.level;
AudioSys.sfx.perfect();}
if(n>0||tspin){
const rowYs=n>0?clearedYs:[VIS_Y+5];
FX.trigger(rowYs,Game.effectIndex,Game.effectLevel,n||1);
if(n>=4){
FX.shake(18+Game.effectLevel*2,0.55);
FX.flash(255,255,255,0.7);FX.chromaBurst(0.8);
FX.addText(W/2,H/2-50,'TETRIS!!',{size:48,color:'#fff',glow:'#0ff',life:1.1,scale:1.1,vy:-18});}
else if(n>0&&!tspin){
FX.addText(W/2,H/2-50,clearType+'!',{size:32,color:'#cdefff',glow:'#8cf',life:0.85,scale:1.05,vy:-14});}
if(tspin&&!tspin.mini){
FX.addText(W/2,H/2+10,clearType+(n>0?' '+n:'')+'!',{size:34,color:'#e8a0ff',glow:'#f0f',life:1.05,scale:1.1,vy:-10});}
else if(tspin&&tspin.mini){
FX.addText(W/2,H/2+10,clearType+'!',{size:26,color:'#c0a0ff',glow:'#80f',life:0.9,vy:-10});}
if(Game.combo>=2){
FX.addText(W/2+250,H/2+40,Game.combo+' COMBO',
{size:24,color:'#ffe66d',glow:'#f90',life:0.85,vy:-18});}
if(Game.b2b>0&&diff&&n>0){
FX.addText(W/2-250,H/2+40,'BACK-TO-BACK',{size:22,color:'#8cf',glow:'#08f',life:0.85,vy:-16});}
if(perfect){
FX.addText(W/2,H/2+85,'PERFECT CLEAR!',{size:34,color:'#fff',glow:'#ff0',life:1.3,scale:1.1,vy:-10});}
if(base>0){
FX.addText(BOARD_X+BOARD_W/2,BOARD_Y+BOARD_H+28,'+'+base,{size:20,color:'#fff',glow:'#ff0',life:0.7,vy:-40});}
Game.message={text:clearType,sub:n>0?n+' LINE'+(n>1?'S':''):'',t:0,life:0.9};}
spawnPiece();}
function holdPiece(){
if(!Game.cur||!Game.canHold||Game.over||Game.paused)return;
AudioSys.sfx.hold();
const type=Game.cur.type;
if(Game.hold){const h=Game.hold;Game.hold=type;spawnPiece(h);}
else{Game.hold=type;spawnPiece();}
Game.canHold=false;Game.lastActionRotate=false;}
function gameOver(){
Game.over=true;Game.started=true;
AudioSys.stopBGM();AudioSys.sfx.gameOver();
FX.shake(20,0.8);FX.flash(255,40,40,0.6);
FX.addText(W/2,H/2-20,'GAME OVER',{size:56,color:'#ff4455',glow:'#f00',life:2.5,vy:0});}
function startGame(){
Game.board=emptyBoard();Game.queue=[];Game.bag=[];Game.hold=null;Game.canHold=true;
Game.score=0;Game.lines=0;Game.level=1;Game.combo=-1;Game.b2b=-1;
Game.over=false;Game.paused=false;Game.started=true;
Game.dropTimer=0;Game.locking=false;Game.lockResets=0;Game.pieceCount=0;
Game.effectIndex=0;Game.effectLevel=0;Game.message=null;Game.startTime=performance.now();
Game.stats={singles:0,doubles:0,triples:0,tetris:0,tspin:0,tspimini:0,perfect:0,pieces:0};
FX.reset();fillQueue();spawnPiece();
AudioSys.resume();AudioSys.sfx.start();AudioSys.setIntensity(0);AudioSys.startBGM();}
const KEY={
left:['ArrowLeft','a','A'],right:['ArrowRight','d','D'],
soft:['ArrowDown','s','S'],rotCW:['ArrowUp','x','X','w','W'],
rotCCW:['z','Z','Control','ControlLeft','ControlRight'],
hard:[' '],hold:['c','C','Shift','ShiftLeft','ShiftRight'],
pause:['p','P','Escape'],restart:['r','R']};
function keyPressed(name){return KEY[name].some(function(k){return Game.keys[k];});}
function justPressed(name){return KEY[name].some(function(k){return Game.keys[k]&&!Game.prevKeys[k];});}
window.addEventListener('keydown',function(e){
if(['ArrowUp','ArrowDown','ArrowLeft','ArrowRight',' '].indexOf(e.key)>=0)e.preventDefault();
AudioSys.resume();
Game.keys[e.key]=true;
if(e.code==='ControlLeft'||e.code==='ControlRight')Game.keys['Control']=true;
if(e.code==='ShiftLeft'||e.code==='ShiftRight')Game.keys['Shift']=true;
if(e.code==='Space')Game.keys[' ']=true;
if(e.code==='ArrowLeft')Game.keys['ArrowLeft']=true;
if(e.code==='ArrowRight')Game.keys['ArrowRight']=true;
if(e.code==='ArrowUp')Game.keys['ArrowUp']=true;
if(e.code==='ArrowDown')Game.keys['ArrowDown']=true;});
window.addEventListener('keyup',function(e){
Game.keys[e.key]=false;
if(e.code==='ControlLeft'||e.code==='ControlRight')Game.keys['Control']=false;
if(e.code==='ShiftLeft'||e.code==='ShiftRight')Game.keys['Shift']=false;
if(e.code==='Space')Game.keys[' ']=false;
if(e.code==='ArrowLeft')Game.keys['ArrowLeft']=false;
if(e.code==='ArrowRight')Game.keys['ArrowRight']=false;
if(e.code==='ArrowUp')Game.keys['ArrowUp']=false;
if(e.code==='ArrowDown')Game.keys['ArrowDown']=false;});
let touchStart=null,touchLast=null;
cv.addEventListener('pointerdown',function(e){
AudioSys.resume();
if(!Game.started||Game.over){startGame();return;}
if(Game.paused){Game.paused=false;AudioSys.startBGM();return;}
touchStart={x:e.clientX,y:e.clientY,t:performance.now()};
touchLast={x:e.clientX,y:e.clientY};});
cv.addEventListener('pointermove',function(e){
if(!touchStart||!Game.cur||Game.paused||Game.over)return;
const dx=e.clientX-touchLast.x,dy=e.clientY-touchLast.y;
const CELLPX=CELL*(cv.getBoundingClientRect().width/W);
while(dx>=CELLPX){tryMove(1,0);touchLast.x+=CELLPX;}
while(dx<=-CELLPX){tryMove(-1,0);touchLast.x-=CELLPX;}
if(dy>CELLPX*1.5){Game.cur.y++;Game.score+=1;Game.dropTimer=0;touchLast.y+=CELLPX*1.5;}});
cv.addEventListener('pointerup',function(e){
if(!touchStart)return;
const dt=performance.now()-touchStart.t;
const dx=e.clientX-touchStart.x,dy=e.clientY-touchStart.y;
const dist=Math.hypot(dx,dy);
if(dt<250&&dist<20)tryRotate(true);
else if(dy>80&&dist>60)hardDrop();
touchStart=null;touchLast=null;});
function roundRect(x,y,w,h,r){
ctx.beginPath();ctx.moveTo(x+r,y);
ctx.arcTo(x+w,y,x+w,y+h,r);ctx.arcTo(x+w,y+h,x,y+h,r);
ctx.arcTo(x,y+h,x,y,r);ctx.arcTo(x,y,x+w,y,r);ctx.closePath();}
function parseHex(hex){
const h=hex.replace('#','');
return[parseInt(h.slice(0,2),16),parseInt(h.slice(2,4),16),parseInt(h.slice(4,6),16)];}
function lighten(hex,amt){const c=parseHex(hex);
return'rgb('+Math.min(255,c[0]+amt)+','+Math.min(255,c[1]+amt)+','+Math.min(255,c[2]+amt)+')';}
function darken(hex,amt){const c=parseHex(hex);
return'rgb('+Math.max(0,c[0]-amt)+','+Math.max(0,c[1]-amt)+','+Math.max(0,c[2]-amt)+')';}
function drawCell(px,py,color,glow,alpha,size){
if(alpha==null)alpha=1;if(size==null)size=CELL;
const pad=1,s=size-pad*2;
ctx.save();ctx.globalAlpha=alpha;
ctx.shadowColor=glow||color;ctx.shadowBlur=10;
const g=ctx.createLinearGradient(px,py,px,py+s);
g.addColorStop(0,lighten(color,35));g.addColorStop(0.45,color);g.addColorStop(1,darken(color,25));
ctx.fillStyle=g;roundRect(px+pad,py+pad,s,s,4);ctx.fill();ctx.shadowBlur=0;
ctx.fillStyle='rgba(255,255,255,.28)';
roundRect(px+pad+2,py+pad+2,s-4,Math.max(3,s*0.22),2);ctx.fill();
ctx.strokeStyle='rgba(255,255,255,.25)';ctx.lineWidth=1;
roundRect(px+pad+0.5,py+pad+0.5,s-1,s-1,4);ctx.stroke();ctx.restore();}
function drawPanel(x,y,w,h,label){
ctx.save();ctx.fillStyle='rgba(10,14,28,.72)';roundRect(x,y,w,h,10);ctx.fill();
ctx.strokeStyle='rgba(120,160,255,.25)';ctx.lineWidth=1.5;roundRect(x,y,w,h,10);ctx.stroke();
ctx.fillStyle='rgba(255,255,255,.06)';roundRect(x+1,y+1,w-2,22,8);ctx.fill();
if(label){ctx.font='700 11px "Segoe UI",system-ui,sans-serif';
ctx.fillStyle='rgba(160,190,255,.85)';ctx.textAlign='center';ctx.textBaseline='middle';
ctx.shadowColor='rgba(80,140,255,.8)';ctx.shadowBlur=8;ctx.fillText(label,x+w/2,y+14);ctx.shadowBlur=0;}
ctx.restore();}
function drawMiniPiece(type,cx,cy,cell){
if(cell==null)cell=18;
const m=ROT_CACHE[type][0];
let minX=9,maxX=-1,minY=9,maxY=-1;
for(let y=0;y<m.length;y++)for(let x=0;x<m[y].length;x++)if(m[y][x]){
if(x<minX)minX=x;if(x>maxX)maxX=x;if(y<minY)minY=y;if(y>maxY)maxY=y;}
const w=(maxX-minX+1)*cell,h=(maxY-minY+1)*cell;
const ox=cx-w/2-minX*cell,oy=cy-h/2-minY*cell;
for(let y=0;y<m.length;y++)for(let x=0;x<m[y].length;x++)if(m[y][x])
drawCell(ox+x*cell,oy+y*cell,COLORS[type],GLOW[type],1,cell);}
function drawBoardFrame(){
ctx.save();
ctx.shadowColor='hsla('+((performance.now()/20)%360)+',100%,60%,'+(0.25+FX.getPhaseGlow()*0.4)+')';
ctx.shadowBlur=24;ctx.strokeStyle='rgba(80,120,255,.5)';ctx.lineWidth=2;
roundRect(BOARD_X-6,BOARD_Y-6,BOARD_W+12,BOARD_H+12,10);ctx.stroke();ctx.restore();
ctx.save();
const bg=ctx.createLinearGradient(BOARD_X,BOARD_Y,BOARD_X,BOARD_Y+BOARD_H);
bg.addColorStop(0,'rgba(8,12,28,.92)');bg.addColorStop(1,'rgba(4,6,16,.96)');
ctx.fillStyle=bg;roundRect(BOARD_X-2,BOARD_Y-2,BOARD_W+4,BOARD_H+4,6);ctx.fill();ctx.restore();
ctx.save();ctx.strokeStyle='rgba(80,110,180,.12)';ctx.lineWidth=1;
for(let x=0;x<=COLS;x++){ctx.beginPath();ctx.moveTo(BOARD_X+x*CELL,BOARD_Y);ctx.lineTo(BOARD_X+x*CELL,BOARD_Y+BOARD_H);ctx.stroke();}
for(let y=0;y<=20;y++){ctx.beginPath();ctx.moveTo(BOARD_X,BOARD_Y+y*CELL);ctx.lineTo(BOARD_X+BOARD_W,BOARD_Y+y*CELL);ctx.stroke();}
ctx.restore();}
function drawStack(){
for(let y=VIS_Y;y<ROWS;y++)for(let x=0;x<COLS;x++){
const t=Game.board[y][x];if(!t)continue;
drawCell(BOARD_X+x*CELL,BOARD_Y+(y-VIS_Y)*CELL,COLORS[t],GLOW[t]);}}
function drawGhost(){
if(!Game.cur)return;
const gy=ghostY(),m=Game.cur.matrix;
ctx.save();ctx.globalAlpha=0.35;
for(let y=0;y<m.length;y++)for(let x=0;x<m[y].length;x++){
if(!m[y][x])continue;const by=gy+y;if(by<VIS_Y)continue;
const px=BOARD_X+(Game.cur.x+x)*CELL,py=BOARD_Y+(by-VIS_Y)*CELL;
ctx.strokeStyle=COLORS[Game.cur.type];ctx.lineWidth=2;
ctx.shadowColor=GLOW[Game.cur.type];ctx.shadowBlur=8;
roundRect(px+3,py+3,CELL-6,CELL-6,4);ctx.stroke();}
ctx.restore();}
function drawCurrent(){
if(!Game.cur)return;
const m=Game.cur.matrix,pulse=0.9+Math.sin(performance.now()/80)*0.1;
for(let y=0;y<m.length;y++)for(let x=0;x<m[y].length;x++){
if(!m[y][x])continue;const by=Game.cur.y+y;if(by<VIS_Y)continue;
drawCell(BOARD_X+(Game.cur.x+x)*CELL,BOARD_Y+(by-VIS_Y)*CELL,
COLORS[Game.cur.type],GLOW[Game.cur.type],pulse);}}
function drawUI(){
const hx=BOARD_X-150,hy=BOARD_Y+8;
drawPanel(hx,hy,130,110,'HOLD');
if(Game.hold){
if(Game.canHold)drawMiniPiece(Game.hold,hx+65,hy+68,16);
else{ctx.save();ctx.globalAlpha=0.3;drawMiniPiece(Game.hold,hx+65,hy+68,16);ctx.restore();}}
const nx=BOARD_X+BOARD_W+20,ny=BOARD_Y+8;
drawPanel(nx,ny,130,320,'NEXT');
for(let i=0;i<6;i++){
const t=Game.queue[i];if(!t)continue;
const scale=i===0?16:13,alpha=i===0?1:0.75-i*0.08;
ctx.save();ctx.globalAlpha=alpha;
drawMiniPiece(t,nx+65,ny+52+i*46,scale);ctx.restore();}
const sx=BOARD_X-150,sy=BOARD_Y+140;
drawPanel(sx,sy,130,240,'STATUS');
ctx.save();ctx.textAlign='left';ctx.textBaseline='middle';
const lines=[['SCORE',Game.score.toLocaleString()],['LINES',String(Game.lines)],
['LEVEL',String(Game.level)],['COMBO',Game.combo>0?'\u00d7'+Game.combo:'\u2014'],
['B2B',Game.b2b>0?'ON':'\u2014']];
lines.forEach(function(kv,i){
const yy=sy+40+i*38;
ctx.font='600 10px "Segoe UI",system-ui,sans-serif';
ctx.fillStyle='rgba(150,180,255,.7)';ctx.fillText(kv[0],sx+14,yy-8);
ctx.font='800 18px "Segoe UI",system-ui,sans-serif';ctx.fillStyle='#fff';
ctx.shadowColor='rgba(120,180,255,.7)';ctx.shadowBlur=8;ctx.fillText(kv[1],sx+14,yy+10);ctx.shadowBlur=0;});
ctx.restore();
const ex=BOARD_X-150,ey=BOARD_Y+400;
drawPanel(ex,ey,130,90,'EFFECT');
const eff=EFFECT_NAMES[Game.effectIndex%EFFECT_NAMES.length];
ctx.save();ctx.textAlign='center';ctx.textBaseline='middle';
ctx.font='900 16px "Segoe UI",system-ui,sans-serif';
const effCol='hsl('+((performance.now()/8)%360)+',100%,70%)';
ctx.fillStyle=effCol;ctx.shadowColor=effCol;ctx.shadowBlur=14;
ctx.fillText(eff,ex+65,ey+42);ctx.shadowBlur=0;
ctx.font='600 11px "Segoe UI",system-ui,sans-serif';
ctx.fillStyle='rgba(200,210,255,.7)';
ctx.fillText('PHASE '+(Game.effectIndex+1)+' \u00b7 +'+(Game.lines%10)+'/10',ex+65,ey+68);
ctx.fillStyle='rgba(255,255,255,.12)';roundRect(ex+14,ey+78,102,5,3);ctx.fill();
ctx.fillStyle=effCol;roundRect(ex+14,ey+78,102*((Game.lines%10)/10),5,3);ctx.fill();
ctx.restore();
ctx.save();ctx.font='600 11px "Segoe UI",system-ui,sans-serif';
ctx.fillStyle='rgba(150,170,220,.55)';ctx.textAlign='right';
const t=Game.startTime?Math.floor((performance.now()-Game.startTime)/1000):0;
const mm=('0'+Math.floor(t/60)).slice(-2),ss=('0'+(t%60)).slice(-2);
ctx.fillText(mm+':'+ss+' \u00b7 '+Game.stats.pieces+' PIECES',BOARD_X+BOARD_W+150,BOARD_Y+BOARD_H+22);
ctx.restore();}
function drawCenterMessage(){
if(!Game.message||Game.message.t>=Game.message.life)return;
const m=Game.message,a=1-m.t/m.life,pop=m.t<0.1?1.5-m.t*5:1;
ctx.save();ctx.globalAlpha=a;ctx.translate(W/2,BOARD_Y+BOARD_H+40);ctx.scale(pop,pop);
ctx.font='900 28px "Segoe UI",system-ui,sans-serif';
ctx.textAlign='center';ctx.textBaseline='middle';
ctx.shadowColor='#fff';ctx.shadowBlur=18;ctx.fillStyle='#fff';ctx.fillText(m.text,0,0);
if(m.sub){ctx.font='700 14px "Segoe UI",system-ui,sans-serif';ctx.fillText(m.sub,0,26);}
ctx.restore();}
function drawTitle(){
if(Game.started&&!Game.over&&!Game.paused)return;
ctx.save();ctx.fillStyle='rgba(3,4,10,.78)';ctx.fillRect(0,0,W,H);
if(!Game.started){
const t=performance.now()/1000;
ctx.textAlign='center';ctx.textBaseline='middle';
ctx.save();ctx.translate(W/2,210+Math.sin(t*2)*6);
ctx.font='900 64px "Segoe UI",system-ui,sans-serif';
const tg=ctx.createLinearGradient(-250,-30,250,30);
tg.addColorStop(0,'hsl('+((t*60)%360)+',100%,65%)');
tg.addColorStop(0.5,'hsl('+((t*60+80)%360)+',100%,70%)');
tg.addColorStop(1,'hsl('+((t*60+160)%360)+',100%,65%)');
ctx.shadowColor=tg;ctx.shadowBlur=30;ctx.fillStyle=tg;
ctx.fillText('EXPLOSIVE',0,-28);ctx.fillText('TETRIS',0,38);ctx.restore();
ctx.font='600 16px "Segoe UI",system-ui,sans-serif';
ctx.fillStyle='rgba(200,215,255,.75)';
ctx.fillText('10\u30e9\u30a4\u30f3\u3054\u3068\u306b\u30a8\u30d5\u30a7\u30af\u30c8\u304c\u9032\u5316\u3059\u308b\u6d3e\u624b\u30c6\u30c8\u30ea\u30b9',W/2,320);
const blink=(Math.sin(t*4)+1)/2;
ctx.globalAlpha=0.5+blink*0.5;
ctx.font='800 22px "Segoe UI",system-ui,sans-serif';ctx.fillStyle='#ffe66d';
ctx.shadowColor='#f90';ctx.shadowBlur=16;
ctx.fillText('CLICK / TAP / SPACE TO START',W/2,420);
ctx.globalAlpha=1;ctx.shadowBlur=0;
ctx.font='500 13px "Segoe UI",system-ui,sans-serif';ctx.fillStyle='rgba(160,180,220,.55)';
['\u2190 \u2192 \u79fb\u52d5\u3000\u3000\u2193 \u30bd\u30d5\u30c8\u30c9\u30ed\u30c3\u30d7\u3000\u3000SPACE \u30cf\u30fc\u30c9\u30c9\u30ed\u30c3\u30d7',
'\u2191 / X \u53f3\u56de\u8ee2\u3000\u3000Z / CTRL \u5de6\u56de\u8ee2\u3000\u3000C / SHIFT \u30db\u30fc\u30eb\u30c9',
'P \u30dd\u30fc\u30ba\u3000\u3000R \u30ea\u30b9\u30bf\u30fc\u30c8'].forEach(function(h,i){
ctx.fillText(h,W/2,500+i*22);});
}
else if(Game.paused){
ctx.textAlign='center';ctx.textBaseline='middle';
ctx.font='900 54px "Segoe UI",system-ui,sans-serif';ctx.fillStyle='#fff';
ctx.shadowColor='#08f';ctx.shadowBlur=24;ctx.fillText('PAUSED',W/2,H/2-20);
ctx.font='600 16px "Segoe UI",system-ui,sans-serif';ctx.fillStyle='rgba(200,210,255,.7)';
ctx.shadowBlur=0;ctx.fillText('P / ESC / \u30af\u30ea\u30c3\u30af\u3067\u518d\u958b',W/2,H/2+30);}
else if(Game.over){
ctx.textAlign='center';ctx.textBaseline='middle';
ctx.font='900 52px "Segoe UI",system-ui,sans-serif';ctx.fillStyle='#ff4455';
ctx.shadowColor='#f00';ctx.shadowBlur=28;ctx.fillText('GAME OVER',W/2,200);
ctx.shadowBlur=0;
ctx.font='800 28px "Segoe UI",system-ui,sans-serif';ctx.fillStyle='#fff';
ctx.fillText('SCORE '+Game.score.toLocaleString(),W/2,280);
ctx.font='600 16px "Segoe UI",system-ui,sans-serif';ctx.fillStyle='rgba(200,215,255,.8)';
const s=Game.stats;
ctx.fillText('LINES '+Game.lines+' \u00b7 LEVEL '+Game.level+' \u00b7 PIECES '+s.pieces,W/2,320);
ctx.fillText('TETRIS '+s.tetris+' \u00b7 T-SPIN '+(s.tspin+s.tspimini)+' \u00b7 PC '+s.perfect,W/2,346);
ctx.fillText('S '+s.singles+' / D '+s.doubles+' / T '+s.triples+' / TE '+s.tetris,W/2,372);
const blink=(Math.sin(performance.now()/250)+1)/2;
ctx.globalAlpha=0.5+blink*0.5;
ctx.font='800 20px "Segoe UI",system-ui,sans-serif';ctx.fillStyle='#ffe66d';
ctx.fillText('CLICK / R TO RETRY',W/2,450);}
ctx.restore();}
let lastTs=0;
function frame(ts){
const dt=Math.min(0.05,(ts-lastTs)/1000)||0.016;
lastTs=ts;
if(Game.started&&!Game.paused&&!Game.over){
if(justPressed('pause')){Game.paused=true;AudioSys.stopBGM();}
if(justPressed('restart'))startGame();
if(justPressed('hold'))holdPiece();
if(justPressed('rotCW'))tryRotate(true);
if(justPressed('rotCCW'))tryRotate(false);
if(justPressed('hard'))hardDrop();
if(justPressed('soft')){
if(Game.cur&&!collides(Game.cur,0,1)){tryMove(0,1);Game.score+=1;}}
const left=keyPressed('left'),right=keyPressed('right');
const dir=left&&!right?-1:right&&!left?1:0;
if(dir!==Game.moveDir){
Game.moveDir=dir;Game.dasTimer=0;Game.arrTimer=0;
if(dir!==0){tryMove(dir,0);AudioSys.sfx.move();}}
else if(dir!==0){
Game.dasTimer+=dt*1000;
if(Game.dasTimer>=DAS_MS){
Game.arrTimer+=dt*1000;
while(Game.arrTimer>=ARR_MS){
Game.arrTimer-=ARR_MS;
if(!tryMove(dir,0))break;}}}
Game.softDrop=keyPressed('soft');
let g=gravityMs();
if(Game.softDrop)g=Math.min(g,1000/SOFT_DROP_FACTOR);
Game.dropTimer+=dt*1000;
if(Game.dropTimer>=g){
Game.dropTimer-=g;
if(Game.cur&&!collides(Game.cur,0,1)){
tryMove(0,1);
if(Game.softDrop)Game.score+=1;}
else if(Game.cur){
Game.locking=true;}}
if(Game.cur){
const grounded=collides(Game.cur,0,1);
if(grounded){
Game.locking=true;
Game.lockTimer+=dt*1000;
if(Game.lockTimer>=LOCK_MS)lockPiece(false);}
else{Game.locking=false;Game.lockTimer=0;}}}
else if(Game.over||Game.paused||!Game.started){
if(Game.over&&(justPressed('restart')||justPressed('hard')))startGame();
if(Game.paused&&justPressed('pause')){Game.paused=false;AudioSys.startBGM();}
if(!Game.started&&(justPressed('hard')||justPressed('rotCW')))startGame();}
if(Game.message)Game.message.t+=dt;
Game.prevKeys={};
for(const k in Game.keys)Game.prevKeys[k]=Game.keys[k];
FX.update(dt);
ctx.clearRect(0,0,W,H);
const sh=FX.getShake();
ctx.save();ctx.translate(sh.x,sh.y);
ctx.fillStyle='#05060a';ctx.fillRect(-20,-20,W+40,H+40);
const cg=ctx.createRadialGradient(W/2,H*0.3,50,W/2,H/2,H*0.7);
cg.addColorStop(0,'rgba(18,22,42,.5)');cg.addColorStop(1,'rgba(0,0,0,0)');
ctx.fillStyle=cg;ctx.fillRect(0,0,W,H);
if(Game.started&&!Game.over){
drawBoardFrame();drawStack();drawGhost();drawCurrent();drawUI();drawCenterMessage();}
FX.draw();
drawTitle();
ctx.restore();
FX.drawOverlay();
if(FX.getChroma()>0){
ctx.save();ctx.globalAlpha=FX.getChroma()*0.25;
ctx.globalCompositeOperation='screen';
ctx.drawImage(cv,2,0);ctx.drawImage(cv,-2,0);
ctx.restore();}
requestAnimationFrame(frame);}
requestAnimationFrame(frame);
window.addEventListener('blur',function(){
if(Game.started&&!Game.over&&!Game.paused){Game.paused=true;AudioSys.stopBGM();}});
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment