Created
September 9, 2026 15:51
-
-
Save kumagi/e8804dcc28c8ef4bf70a5ca414c175cc to your computer and use it in GitHub Desktop.
Muse Spark 1.3, 2分53秒で完成
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!DOCTYPE html> | |
| <html lang="ja"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"> | |
| <title>派手エフェクト・テトリス</title> | |
| <style> | |
| :root{ | |
| --bg:#070714; --panel:rgba(20,20,45,.85); --neon-cyan:#00f0ff; --neon-pink:#ff2fb3; | |
| --neon-yellow:#ffe600; --neon-green:#39ff6a; | |
| } | |
| *{box-sizing:border-box; -webkit-tap-highlight-color:transparent;} | |
| html,body{margin:0;padding:0;background:var(--bg);color:#fff; | |
| font-family:"Hiragino Kaku Gothic ProN","Noto Sans JP",Meiryo,sans-serif; | |
| min-height:100vh;overflow-x:hidden;} | |
| body{ | |
| background:radial-gradient(1200px 800px at 50% -10%, #1b1b4d 0%, #070714 60%) fixed; | |
| } | |
| #bg-fx{position:fixed;inset:0;pointer-events:none;z-index:0;opacity:.9;} | |
| #flash{position:fixed;inset:0;background:#fff;opacity:0;pointer-events:none;z-index:50;transition:opacity .05s;} | |
| #vignette{position:fixed;inset:0;pointer-events:none;z-index:5; | |
| background:radial-gradient(ellipse at center, transparent 55%, rgba(0,0,0,.55) 100%);} | |
| .wrap{position:relative;z-index:10;max-width:980px;margin:0 auto;padding:12px; | |
| display:flex;gap:12px;justify-content:center;align-items:flex-start;flex-wrap:wrap;} | |
| header{width:100%;text-align:center;margin:4px 0 2px;} | |
| header h1{margin:0;font-size:clamp(20px,4vw,34px);letter-spacing:.1em; | |
| background:linear-gradient(90deg,var(--neon-cyan),var(--neon-pink),var(--neon-yellow),var(--neon-cyan)); | |
| background-size:300% 100%;-webkit-background-clip:text;background-clip:text;color:transparent; | |
| animation:rainbow 6s linear infinite;filter:drop-shadow(0 0 12px rgba(255,47,179,.6));} | |
| @keyframes rainbow{to{background-position:300% 0;}} | |
| header p{margin:2px 0 6px;color:#aaa;font-size:12px;letter-spacing:.15em;} | |
| #effect-banner{display:inline-block;margin-top:4px;padding:4px 16px;border-radius:999px; | |
| font-weight:900;letter-spacing:.15em;font-size:13px;color:#000; | |
| background:linear-gradient(90deg,var(--neon-yellow),#fff,var(--neon-yellow)); | |
| box-shadow:0 0 18px var(--neon-yellow);transition:transform .15s;} | |
| #effect-banner.pop{transform:scale(1.25) rotate(-2deg);} | |
| .panel{background:var(--panel);border:1px solid rgba(0,240,255,.25);border-radius:12px; | |
| padding:10px;backdrop-filter:blur(6px);box-shadow:0 0 20px rgba(0,240,255,.12);} | |
| .side{width:150px;display:flex;flex-direction:column;gap:10px;} | |
| .side h3{margin:0 0 4px;font-size:12px;color:var(--neon-cyan);letter-spacing:.2em;text-align:center;} | |
| canvas.mini{display:block;margin:0 auto;background:#0a0a1e;border-radius:8px;} | |
| #stage-wrap{position:relative;} | |
| #stage{display:block;background:#0a0a1e;border-radius:12px; | |
| border:2px solid rgba(0,240,255,.5);box-shadow:0 0 30px rgba(0,240,255,.25), inset 0 0 40px rgba(0,0,0,.6);} | |
| #stage.shake{animation:shake .35s;} | |
| @keyframes shake{ | |
| 0%{transform:translate(0,0)}20%{transform:translate(-8px,4px) rotate(-.5deg)} | |
| 40%{transform:translate(7px,-6px)}60%{transform:translate(-5px,-3px)} | |
| 80%{transform:translate(4px,3px)}100%{transform:translate(0,0)}} | |
| #float-layer{position:absolute;inset:0;pointer-events:none;overflow:hidden;border-radius:12px;} | |
| .float-txt{position:absolute;left:50%;top:40%;transform:translate(-50%,-50%); | |
| font-weight:900;white-space:nowrap;text-align:center; | |
| text-shadow:0 0 10px currentColor,0 2px 0 #000; | |
| animation:floatup 1.4s ease-out forwards;} | |
| @keyframes floatup{ | |
| 0%{transform:translate(-50%,-50%) scale(.5);opacity:0;} | |
| 15%{transform:translate(-50%,-60%) scale(1.2);opacity:1;} | |
| 30%{transform:translate(-50%,-65%) scale(1);} | |
| 100%{transform:translate(-50%,-160%) scale(1);opacity:0;}} | |
| .stats{font-size:13px;line-height:1.7;} | |
| .stats b{color:var(--neon-yellow);font-size:16px;} | |
| #b2b{color:var(--neon-pink);font-weight:900;} | |
| .btns{display:flex;gap:6px;flex-wrap:wrap;justify-content:center;} | |
| button{cursor:pointer;border:1px solid rgba(0,240,255,.4);background:#141433;color:#fff; | |
| border-radius:8px;padding:7px 10px;font-size:13px;font-weight:700;} | |
| button:hover{background:#1f1f55;box-shadow:0 0 10px rgba(0,240,255,.5);} | |
| button.primary{background:linear-gradient(90deg,#ff2fb3,#7b2fff);border:none;} | |
| #overlay{position:absolute;inset:0;display:flex;flex-direction:column;gap:10px; | |
| align-items:center;justify-content:center;background:rgba(5,5,20,.82); | |
| border-radius:12px;text-align:center;z-index:20;padding:16px;} | |
| #overlay.hidden{display:none;} | |
| #overlay h2{margin:0;font-size:30px;letter-spacing:.1em; | |
| text-shadow:0 0 18px var(--neon-pink);} | |
| #overlay p{margin:0;color:#ccc;font-size:13px;line-height:1.9;} | |
| kbd{background:#222;border:1px solid #555;border-bottom-width:3px;border-radius:5px; | |
| padding:1px 6px;font-size:11px;font-family:monospace;} | |
| #touch{display:none;width:100%;justify-content:center;gap:8px;margin-top:8px;flex-wrap:wrap;} | |
| #touch button{font-size:18px;padding:10px 14px;touch-action:manipulation;user-select:none;} | |
| @media (max-width:720px){ | |
| .side{width:32vw;min-width:110px;} #touch{display:flex;} | |
| #stage{width:52vw !important;height:auto !important;max-width:300px;} | |
| } | |
| .combo-fire{color:var(--neon-green) !important;text-shadow:0 0 12px var(--neon-green) !important;} | |
| </style> | |
| </head> | |
| <body> | |
| <canvas id="bg-fx"></canvas> | |
| <div id="vignette"></div> | |
| <div id="flash"></div> | |
| <header> | |
| <h1>💥 派手エフェクト・テトリス 💥</h1> | |
| <p>TETRIS GUIDELINE準拠 / 7-BAG・SRS・GHOST・HOLD・T-SPIN</p> | |
| <div><span id="effect-banner">Lv.0 SPARK BURST</span></div> | |
| </header> | |
| <div class="wrap"> | |
| <div class="side panel"> | |
| <div><h3>HOLD (C)</h3><canvas id="hold" class="mini" width="120" height="80"></canvas></div> | |
| <div class="stats"> | |
| SCORE<br><b id="score">0</b><br> | |
| LINES<br><b id="lines">0</b><br> | |
| LEVEL<br><b id="level">1</b><br> | |
| TIME<br><b id="time">0:00</b><br> | |
| COMBO<br><b id="combo">0</b><br> | |
| <span id="b2b"></span> | |
| </div> | |
| <div class="btns"> | |
| <button id="btn-pause">⏸ P</button> | |
| <button id="btn-restart" class="primary">↻ R</button> | |
| <button id="btn-mute">🔊 M</button> | |
| </div> | |
| </div> | |
| <div class="panel" id="stage-wrap"> | |
| <div style="position:relative"> | |
| <canvas id="stage" width="300" height="600"></canvas> | |
| <div id="float-layer"></div> | |
| <div id="overlay"> | |
| <h2 id="ov-title">▶ START</h2> | |
| <p> | |
| <kbd>←</kbd><kbd>→</kbd> 移動 / <kbd>↓</kbd> ソフトドロップ<br> | |
| <kbd>↑</kbd>/<kbd>X</kbd> 右回転 <kbd>Z</kbd> 左回転 <kbd>A</kbd> 180°<br> | |
| <kbd>Space</kbd> ハードドロップ / <kbd>C</kbd> ホールド<br> | |
| <kbd>P</kbd> ポーズ <kbd>R</kbd> リスタート<br><br> | |
| 10ラインごとにエフェクト進化!<br>TETRIS・T-SPINで画面爆発💥 | |
| </p> | |
| <button id="btn-start" class="primary" style="font-size:18px;padding:12px 32px">ゲーム開始!!</button> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="side panel"> | |
| <div><h3>NEXT</h3><canvas id="next" class="mini" width="120" height="340"></canvas></div> | |
| <div class="stats" style="font-size:11px;color:#bbb"> | |
| EFFECT TIERS<br> | |
| 0-9: SPARK<br> | |
| 10-19: FIREWORKS🎆<br> | |
| 20-29: SHOCKWAVE🌊<br> | |
| 30-39: LASER RAIN🔫<br> | |
| 40+: SUPERNOVA🌟 | |
| </div> | |
| </div> | |
| <div id="touch"> | |
| <button data-k="left">◀</button><button data-k="right">▶</button> | |
| <button data-k="down">▼</button><button data-k="rotR">⟳</button> | |
| <button data-k="rotL">⟲</button><button data-k="hard">⤓</button> | |
| <button data-k="hold">H</button> | |
| </div> | |
| </div> | |
| <script> | |
| "use strict"; | |
| /* ============================================================ | |
| 派手エフェクト・テトリス (single file, no deps, WebAudio合成) | |
| Guideline: 7-bag / SRS + kicks / ghost / hold / lock delay / | |
| DAS+ARR / T-spin(3-corner) / B2B / Combo / Perfect Clear | |
| ============================================================ */ | |
| const COLS=10, ROWS=20, HIDDEN=6, CELL=30; | |
| const W=COLS*CELL, H=ROWS*CELL; | |
| // ---------- テトリミノ定義 (SRS spawn orientation) ---------- | |
| const SHAPES={ | |
| 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 COLORS={I:"#00f0ff",J:"#2f6bff",L:"#ff8c00",O:"#ffe600",S:"#39ff6a",T:"#c724ff",Z:"#ff2f4e"}; | |
| const DARK ={I:"#005a66",J:"#122a88",L:"#7a3c00",O:"#7a7400",S:"#0d6a26",T:"#4d0a66",Z:"#7a0e20"}; | |
| // SRS wall kicks (x right+, y down+ ; 表は上向き=+yに注意し符号調整済み) | |
| const KICKS_JLSTZ={ | |
| "0-1":[[0,0],[-1,0],[-1,1],[0,-2],[-1,-2]], | |
| "1-0":[[0,0],[1,0],[1,-1],[0,2],[1,2]], | |
| "1-2":[[0,0],[1,0],[1,-1],[0,2],[1,2]], | |
| "2-1":[[0,0],[-1,0],[-1,1],[0,-2],[-1,-2]], | |
| "2-3":[[0,0],[1,0],[1,1],[0,-2],[1,-2]], | |
| "3-2":[[0,0],[-1,0],[-1,-1],[0,2],[-1,2]], | |
| "3-0":[[0,0],[-1,0],[-1,-1],[0,2],[-1,2]], | |
| "0-3":[[0,0],[1,0],[1,1],[0,-2],[1,-2]], | |
| }; | |
| const KICKS_I={ | |
| "0-1":[[0,0],[-2,0],[1,0],[-2,-1],[1,2]], | |
| "1-0":[[0,0],[2,0],[-1,0],[2,1],[-1,-2]], | |
| "1-2":[[0,0],[-1,0],[2,0],[-1,2],[2,-1]], | |
| "2-1":[[0,0],[1,0],[-2,0],[1,-2],[-2,1]], | |
| "2-3":[[0,0],[2,0],[-1,0],[2,1],[-1,-2]], | |
| "3-2":[[0,0],[-2,0],[1,0],[-2,-1],[1,2]], | |
| "3-0":[[0,0],[1,0],[-2,0],[1,-2],[-2,1]], | |
| "0-3":[[0,0],[-1,0],[2,0],[-1,2],[2,-1]], | |
| }; | |
| // エフェクトTier名 | |
| const TIERS=["SPARK BURST","FIREWORKS🎆","SHOCKWAVE🌊","LASER RAIN🔫","SUPERNOVA🌟"]; | |
| const tierOf = lines => Math.min(4, Math.floor(lines/10)); | |
| // ---------- 状態 ---------- | |
| let grid, bag, queue, cur, holdPiece, canHold; | |
| let score, lines, level, combo, b2b, startTime, elapsed, lastTick; | |
| let state="ready"; // ready|play|pause|over | |
| let dropCounter, lockTimer, lockResets, lastAction; // lastAction: move|rotate|fall | |
| let dasDir=0, dasTimer=0, arrTimer=0; | |
| let renCombo=0; | |
| const DAS=140, ARR=35, LOCK_DELAY=500, MAX_RESETS=15; | |
| // 重力: levelごとの落下間隔(sec) | |
| function gravitySec(lv){ | |
| // guideline formula (0.8-(lv-1)*0.007)^(lv-1) | |
| return Math.max(1/60, Math.pow(0.8-(lv-1)*0.007, lv-1)); | |
| } | |
| // ---------- キャンバス ---------- | |
| const stage=document.getElementById("stage"), ctx=stage.getContext("2d"); | |
| const holdCv=document.getElementById("hold"), hctx=holdCv.getContext("2d"); | |
| const nextCv=document.getElementById("next"), nctx=nextCv.getContext("2d"); | |
| const bgCv=document.getElementById("bg-fx"), bctx=bgCv.getContext("2d"); | |
| function fitBg(){bgCv.width=innerWidth;bgCv.height=innerHeight;} | |
| addEventListener("resize",fitBg);fitBg(); | |
| const flashEl=document.getElementById("flash"); | |
| function flash(op=0.7,color="#fff",ms=90){ | |
| flashEl.style.background=color;flashEl.style.opacity=op; | |
| setTimeout(()=>flashEl.style.opacity=0,ms); | |
| } | |
| // ---------- 背景スター ---------- | |
| const stars=Array.from({length:140},()=>({ | |
| x:Math.random()*innerWidth,y:Math.random()*innerHeight, | |
| s:Math.random()*2+0.5,v:Math.random()*30+5,hue:Math.random()*360})); | |
| let bgHue=190; | |
| function drawBg(dt){ | |
| bctx.fillStyle="rgba(7,7,20,0.35)";bctx.fillRect(0,0,bgCv.width,bgCv.height); | |
| for(const st of stars){ | |
| st.y+=st.v*dt*(1+lines/60); if(st.y>bgCv.height){st.y=0;st.x=Math.random()*bgCv.width;} | |
| bctx.fillStyle=`hsl(${(st.hue+bgHue)%360} 100% 70%)`; | |
| bctx.fillRect(st.x,st.y,st.s,st.s); | |
| } | |
| bgHue=(bgHue+dt*10)%360; | |
| } | |
| // ---------- パーティクル ---------- | |
| let particles=[]; | |
| function spawnParticles(x,y,opts={}){ | |
| const tier=tierOf(lines); | |
| const n=opts.n ?? (20+tier*25+ (opts.lines||0)*30); | |
| const colors=opts.colors ?? [COLORS[cur?.type]||"#fff","#ffffff","#ffe600","#ff2fb3","#00f0ff"]; | |
| for(let i=0;i<n;i++){ | |
| const a=Math.random()*Math.PI*2, sp=(opts.power??380)*(0.3+Math.random()); | |
| particles.push({ | |
| x,y,vx:Math.cos(a)*sp,vy:Math.sin(a)*sp-160, | |
| life:0.6+Math.random()*0.9,t:0, | |
| size:2+Math.random()*(4+tier*2), | |
| color:colors[(Math.random()*colors.length)|0], | |
| shape:Math.random()<.25?"star":(Math.random()<.5?"rect":"circle"), | |
| rot:Math.random()*6,vr:(Math.random()-.5)*12, | |
| grav:opts.grav??700 | |
| }); | |
| } | |
| // Tier別追加演出 | |
| if(tier>=1|| (opts.lines||0)>=2){ // 花火: 上向きロケット | |
| for(let i=0;i<4+tier*3;i++) particles.push({ | |
| x:x+(Math.random()-.5)*200,y:H,vx:(Math.random()-.5)*60,vy:-500-Math.random()*300, | |
| life:1.2,t:0,size:3,color:colors[i%colors.length],shape:"rocket",rot:0,vr:0,grav:200,boom:true}); | |
| } | |
| if(tier>=2){ shockwaves.push({x,y,r:10,alpha:1,lw:6}); } | |
| if(tier>=3){ // レーザー雨 | |
| for(let i=0;i<12;i++) lasers.push({x:Math.random()*W,delay:Math.random()*0.5,t:0}); | |
| } | |
| if(tier>=4){ flash(0.5,"#fff",120); shake(9); } | |
| } | |
| let shockwaves=[], lasers=[]; | |
| function shake(mag){ | |
| const s=stage; | |
| s.style.setProperty("--sx",mag+"px"); | |
| s.classList.remove("shake");void s.offsetWidth;s.classList.add("shake"); | |
| } | |
| // 花火ロケットが頂点で爆発 | |
| function updateParticles(dt){ | |
| for(const p of particles){ | |
| p.t+=dt; | |
| if(p.shape==="rocket"){ | |
| p.vy+=p.grav*dt; p.x+=p.vx*dt; p.y+=p.vy*dt; | |
| if(p.vy>-40 && p.boom){ // 爆発 | |
| p.boom=false;p.life=0;p.t=1; | |
| for(let i=0;i<26;i++){const a=Math.random()*6.28,sp=200+Math.random()*220; | |
| particles.push({x:p.x,y:p.y,vx:Math.cos(a)*sp,vy:Math.sin(a)*sp, | |
| life:.8,t:0,size:2.5,color:p.color,shape:"circle",rot:0,vr:0,grav:300});} | |
| } | |
| continue; | |
| } | |
| p.vy+=p.grav*dt;p.x+=p.vx*dt;p.y+=p.vy*dt;p.rot+=p.vr*dt; | |
| if(p.y>H){p.y=H;p.vy*=-0.5;p.vx*=0.8;} | |
| } | |
| particles=particles.filter(p=>p.t<p.life); | |
| if(particles.length>1500) particles.splice(0,particles.length-1500); | |
| for(const s of shockwaves){s.r+=900*dt;s.alpha-=2.2*dt;} | |
| shockwaves=shockwaves.filter(s=>s.alpha>0); | |
| for(const l of lasers){l.t+=dt;} | |
| lasers=lasers.filter(l=>l.t<0.8); | |
| } | |
| function drawParticles(){ | |
| for(const p of particles){ | |
| const a=1-p.t/p.life; | |
| ctx.save();ctx.globalAlpha=Math.max(0,a); | |
| ctx.translate(p.x,p.y);ctx.rotate(p.rot||0); | |
| ctx.fillStyle=p.color;ctx.shadowColor=p.color;ctx.shadowBlur=8; | |
| if(p.shape==="circle"){ctx.beginPath();ctx.arc(0,0,p.size/2,0,7);ctx.fill();} | |
| else if(p.shape==="star"){drawStar(0,0,p.size,5);ctx.fill();} | |
| else{ctx.fillRect(-p.size/2,-p.size/2,p.size,p.size*0.6);} | |
| ctx.restore(); | |
| } | |
| for(const s of shockwaves){ | |
| ctx.save();ctx.globalAlpha=Math.max(0,s.alpha); | |
| ctx.strokeStyle="#00f0ff";ctx.lineWidth=s.lw;ctx.shadowColor="#00f0ff";ctx.shadowBlur=20; | |
| ctx.beginPath();ctx.arc(s.x,s.y,s.r,0,7);ctx.stroke();ctx.restore(); | |
| } | |
| for(const l of lasers){ | |
| if(l.t<l.delay)continue; | |
| const k=(l.t-l.delay)/0.5; | |
| ctx.save();ctx.globalAlpha=1-k;ctx.strokeStyle="#ff2fb3";ctx.lineWidth=3; | |
| ctx.shadowColor="#ff2fb3";ctx.shadowBlur=14; | |
| ctx.beginPath();ctx.moveTo(l.x,0);ctx.lineTo(l.x,H);ctx.stroke();ctx.restore(); | |
| } | |
| } | |
| function drawStar(x,y,r,points){ | |
| ctx.beginPath(); | |
| for(let i=0;i<points*2;i++){const rr=i%2?r*0.45:r;const a=i*Math.PI/points-Math.PI/2; | |
| ctx[i?"lineTo":"moveTo"](x+Math.cos(a)*rr,y+Math.sin(a)*rr);} | |
| ctx.closePath(); | |
| } | |
| // フローティングテキスト | |
| function floatText(txt,color="#fff",size=26){ | |
| const layer=document.getElementById("float-layer"); | |
| const d=document.createElement("div"); | |
| d.className="float-txt";d.textContent=txt; | |
| d.style.color=color;d.style.fontSize=size+"px"; | |
| d.style.top=(30+Math.random()*30)+"%"; | |
| layer.appendChild(d);setTimeout(()=>d.remove(),1450); | |
| } | |
| // ---------- サウンド (全合成WebAudio) ---------- | |
| let AC=null, muted=false, bgmTimer=null, bgmStep=0, bgmNextTime=0; | |
| function ac(){ if(!AC){AC=new (window.AudioContext||window.webkitAudioContext)();} if(AC.state==="suspended")AC.resume(); return AC; } | |
| function tone(freq,dur,type="square",vol=0.12,when=0,slideTo=null){ | |
| if(muted)return; const c=ac(),t=c.currentTime+when; | |
| const o=c.createOscillator(),g=c.createGain(); | |
| o.type=type;o.frequency.setValueAtTime(freq,t); | |
| if(slideTo)o.frequency.exponentialRampToValueAtTime(slideTo,t+dur); | |
| g.gain.setValueAtTime(vol,t);g.gain.exponentialRampToValueAtTime(0.0001,t+dur); | |
| o.connect(g).connect(c.destination);o.start(t);o.stop(t+dur+0.02); | |
| } | |
| function noise(dur=0.2,vol=0.2,when=0,hp=1000){ | |
| if(muted)return; const c=ac(),t=c.currentTime+when; | |
| const buf=c.createBuffer(1,c.sampleRate*dur,c.sampleRate); | |
| const d=buf.getChannelData(0);for(let i=0;i<d.length;i++)d[i]=Math.random()*2-1; | |
| const src=c.createBufferSource();src.buffer=buf; | |
| const f=c.createBiquadFilter();f.type="highpass";f.frequency.value=hp; | |
| const g=c.createGain();g.gain.setValueAtTime(vol,t);g.gain.exponentialRampToValueAtTime(0.0001,t+dur); | |
| src.connect(f).connect(g).connect(c.destination);src.start(t); | |
| } | |
| const SE={ | |
| move(){tone(220,0.03,"square",0.05);}, | |
| rotate(){tone(440,0.06,"square",0.08,0,660);}, | |
| hold(){tone(330,0.1,"triangle",0.12,0,520);}, | |
| soft(){tone(180,0.02,"square",0.03);}, | |
| hard(){noise(0.15,0.3,0,300);tone(120,0.15,"sine",0.25,0,40);}, | |
| lock(){tone(140,0.08,"triangle",0.12);}, | |
| clear(n){for(let i=0;i<n+2;i++)tone(523*Math.pow(1.335,i),0.12,"square",0.1,i*0.06);noise(0.3,0.15,0,2000);}, | |
| tetris(){[523,659,784,1046,1318].forEach((f,i)=>tone(f,0.18,"sawtooth",0.1,i*0.07));noise(0.6,0.25,0,800);}, | |
| tspin(){[392,523,659,784,1046,1568].forEach((f,i)=>tone(f,0.2,"square",0.1,i*0.06));}, | |
| combo(c){tone(600+c*80,0.1,"square",0.1);tone(900+c*80,0.12,"square",0.08,0.05);}, | |
| levelup(){[523,659,784,1046].forEach((f,i)=>tone(f,0.15,"triangle",0.14,i*0.09));}, | |
| perfect(){[523,659,784,1046,1318,1568,2093].forEach((f,i)=>tone(f,0.25,"triangle",0.12,i*0.08));}, | |
| over(){[400,300,200,120].forEach((f,i)=>tone(f,0.3,"sawtooth",0.12,i*0.2));}, | |
| }; | |
| // BGM: オリジナル8bitループ。Tierが上がるとレイヤー追加+テンポUP | |
| const BASS=[0,0,7,0, 5,5,0,3, 2,2,0,-2, 3,3,7,5]; // semitone offset from A2 | |
| const LEAD=[12,15,19,15, 12,10,12,15, 12,15,19,24, 22,19,15,12]; | |
| function startBGM(){ | |
| stopBGM(); const c=ac(); bgmStep=0; bgmNextTime=c.currentTime+0.1; | |
| const tier=tierOf(lines); | |
| const bpm=132+tier*8; | |
| const spb=60/bpm/2; | |
| bgmTimer=setInterval(()=>{ | |
| if(muted||state!=="play") {bgmNextTime=Math.max(bgmNextTime,c.currentTime+0.05);return;} | |
| while(bgmNextTime<c.currentTime+0.15){ | |
| const s=bgmStep%16, when=Math.max(0,bgmNextTime-c.currentTime); | |
| const base=110; | |
| tone(base*Math.pow(2,BASS[s]/12),0.18,"triangle",0.10,when); // bass | |
| if(tier>=0 && s%2===0) tone(6600,0.03,"square",0.03,when); // hat | |
| if(tier>=1) tone(s%4===0?180:120,0.1,"sine",0.22,when,s%4===0?50:60); // kick-ish | |
| if(tier>=2) tone(base*4*Math.pow(2,LEAD[s]/12),0.16,"square",0.05,when); // lead | |
| if(tier>=3 && s%2===1) noise(0.08,0.08,when,4000); // snare-ish | |
| if(tier>=4) tone(base*8*Math.pow(2,LEAD[(s+8)%16]/12),0.1,"sawtooth",0.03,when); // arp | |
| bgmNextTime+=spb; bgmStep++; | |
| } | |
| },40); | |
| } | |
| function stopBGM(){if(bgmTimer){clearInterval(bgmTimer);bgmTimer=null;}} | |
| // ---------- ゲームロジック ---------- | |
| function newBag(){ | |
| const b=["I","J","L","O","S","T","Z"]; | |
| for(let i=b.length-1;i>0;i--){const j=(Math.random()*(i+1))|0;[b[i],b[j]]=[b[j],b[i]];} | |
| return b; | |
| } | |
| function reset(){ | |
| grid=Array.from({length:ROWS+HIDDEN},()=>Array(COLS).fill(null)); | |
| bag=newBag();queue=[]; | |
| refill(); | |
| holdPiece=null;canHold=true; | |
| score=0;lines=0;level=1;combo=-1;b2b=0;renCombo=0; | |
| elapsed=0;startTime=performance.now(); | |
| particles=[];shockwaves=[];lasers=[]; | |
| spawnPiece();updateHUD(); | |
| } | |
| function refill(){while(queue.length<7){if(!bag.length)bag=newBag();queue.push(bag.pop());}} | |
| function spawnPiece(){ | |
| refill(); | |
| const type=queue.shift();refill(); | |
| const m=SHAPES[type]; | |
| cur={type,matrix:m.map(r=>r.slice()),rot:0, | |
| x:3-((m[0].length-2)>>1),y:type==="I"?-1:0,px:0,py:0}; | |
| if(type==="O")cur.x=4; | |
| if(type==="I")cur.x=3; | |
| dropCounter=0;lockTimer=0;lockResets=0;lastAction="fall"; | |
| if(collides(cur.matrix,cur.x,cur.y)){gameOver();return;} | |
| drawNext();drawHold(); | |
| } | |
| function cellsOf(matrix){const r=[];for(let y=0;y<matrix.length;y++)for(let x=0;x<matrix[y].length;x++)if(matrix[y][x])r.push([x,y]);return r;} | |
| function collides(matrix,px,py){ | |
| for(const [x,y] of cellsOf(matrix)){ | |
| const bx=px+x,by=py+y+HIDDEN; | |
| if(bx<0||bx>=COLS||by>=(ROWS+HIDDEN))return true; | |
| if(by>=0&&grid[by][bx])return true; | |
| } | |
| return false; | |
| } | |
| function rotateMatrix(m,dir){ // dir 1:CW -1:CCW 2:180 | |
| const N=m.length,R=m.map(r=>r.slice()); | |
| if(dir===2)return m.map((row,y)=>row.map((_,x)=>m[N-1-y][N-1-x])); | |
| const out=Array.from({length:N},()=>Array(N).fill(0)); | |
| for(let y=0;y<N;y++)for(let x=0;x<N;x++) | |
| out[y][x]=dir===1?m[N-1-x][y]:m[x][N-1-y]; | |
| return out; | |
| } | |
| function tryRotate(dir){ | |
| if(state!=="play"||!cur)return; | |
| if(cur.type==="O"){SE.rotate();lastAction="rotate";return;} | |
| const from=cur.rot, to=(from+(dir===1?1:dir===-1?3:2))%4; | |
| const nm=rotateMatrix(cur.matrix,dir); | |
| const table=cur.type==="I"?KICKS_I:KICKS_JLSTZ; | |
| const tests=table[`${from}-${to}`]||[[0,0]]; | |
| for(const [dx,dy] of tests){ | |
| if(!collides(nm,cur.x+dx,cur.y-dy)){ // SRSのyは上向き+なので反転 | |
| cur.matrix=nm;cur.x+=dx;cur.y-=dy;cur.rot=to; | |
| lastAction="rotate"; | |
| if(onGround()){if(lockResets<MAX_RESETS){lockTimer=0;lockResets++;}} | |
| SE.rotate();return; | |
| } | |
| } | |
| } | |
| function onGround(){return collides(cur.matrix,cur.x,cur.y+1);} | |
| function move(dx){ | |
| if(state!=="play"||!cur)return false; | |
| if(!collides(cur.matrix,cur.x+dx,cur.y)){cur.x+=dx;lastAction="move"; | |
| if(onGround()&&lockResets<MAX_RESETS){lockTimer=0;lockResets++;} SE.move();return true;} | |
| return false; | |
| } | |
| function softDrop(){ | |
| if(state!=="play"||!cur)return; | |
| if(!collides(cur.matrix,cur.x,cur.y+1)){cur.y++;score+=1;lastAction="fall";dropCounter=0;SE.soft();} | |
| updateHUD(); | |
| } | |
| function hardDrop(){ | |
| if(state!=="play"||!cur)return; | |
| let d=0;while(!collides(cur.matrix,cur.x,cur.y+1)){cur.y++;d++;} | |
| score+=d*2;SE.hard(); | |
| const gx=cur.x*CELL+CELL, gy=(cur.y+HIDDEN)*CELL; | |
| spawnParticles(gx,gy,{n:30,power:420}); | |
| shake(Math.min(6,2+d*0.15)); | |
| lockPiece(); | |
| } | |
| function hold(){ | |
| if(state!=="play"||!cur||!canHold)return; | |
| SE.hold(); | |
| if(!holdPiece){holdPiece=cur.type;spawnPiece();} | |
| else{const t=holdPiece;holdPiece=cur.type; | |
| cur={type:t,matrix:SHAPES[t].map(r=>r.slice()),rot:0,x:t==="O"?4:3,y:t==="I"?-1:0}; | |
| if(collides(cur.matrix,cur.x,cur.y)){gameOver();return;}} | |
| canHold=false;drawHold(); | |
| } | |
| // T-spin判定 (3-corner rule) | |
| function isTspin(){ | |
| if(!cur||cur.type!=="T"||lastAction!=="rotate")return false; | |
| const cx=cur.x+1, cy=cur.y+1+HIDDEN; // T中心 | |
| let occ=0; | |
| for(const [dx,dy] of [[-1,-1],[1,-1],[-1,1],[1,1]]){ | |
| const x=cx+dx,y=cy+dy; | |
| if(x<0||x>=COLS||y>=(ROWS+HIDDEN)||y<0||(y>=0&&grid[y][x]))occ++; | |
| } | |
| return occ>=3; | |
| } | |
| function lockPiece(){ | |
| const tspin=isTspin(); | |
| for(const [x,y] of cellsOf(cur.matrix)){ | |
| const bx=cur.x+x,by=cur.y+y+HIDDEN; | |
| if(by<0){gameOver();return;} | |
| grid[by][bx]=cur.type; | |
| } | |
| SE.lock(); | |
| // ライン検出 | |
| const full=[]; | |
| for(let y=0;y<ROWS+HIDDEN;y++)if(grid[y].every(c=>c))full.push(y); | |
| const cleared=full.length; | |
| const perfectBefore=grid.every(row=>row.every(c=>!c)); | |
| if(cleared>0){ | |
| // 消去アニメ用にフラッシュ行を記録 | |
| clearAnim={rows:full.slice(),t:0}; | |
| for(const y of full)grid.splice(y,1); | |
| while(grid.length<ROWS+HIDDEN)grid.unshift(Array(COLS).fill(null)); | |
| const difficult=(cleared===4||tspin); | |
| if(tspin||cleared===4)b2b++;else if(cleared>0&&!tspin)b2b=0; | |
| combo++; | |
| const lv=level; | |
| let pts=0,label="",color="#fff"; | |
| if(tspin){ | |
| const base=[0,800,1200,1600][cleared]||400; | |
| pts=base*lv*(b2b>0?1.5:1); | |
| label=`T-SPIN ${["","SINGLE","DOUBLE","TRIPLE"][cleared]||""}${b2b>0?" B2B!":""}`; | |
| color="#c724ff";SE.tspin(); | |
| }else{ | |
| const base=[0,100,300,500,800][cleared]||0; | |
| pts=base*lv*(b2b>0&&cleared===4?1.5:1); | |
| label=["","SINGLE!","DOUBLE!","TRIPLE!","TETRIS!!"][cleared]||`${cleared} LINES!`; | |
| color=cleared===4?"#ffe600":"#00f0ff"; | |
| if(cleared===4)SE.tetris();else SE.clear(cleared); | |
| } | |
| pts+=Math.max(0,combo)*50*lv; | |
| score+=pts;lines+=cleared; | |
| const nl=Math.floor(lines/10)+1; | |
| if(nl>level){level=nl;SE.levelup();floatText(`LEVEL ${level}!`,"#ffe600",34);startBGM();} | |
| // ---- 派手エフェクト ---- | |
| const prevTier=tierOf(lines-cleared), nowTier=tierOf(lines); | |
| triggerClearFX(cleared,full,label,color); | |
| if(nowTier>prevTier){ | |
| floatText(`✨ ${TIERS[nowTier]} 解放! ✨`,"#ffe600",30); | |
| flash(0.6,"#ffe600",150);SE.levelup();startBGM(); | |
| } | |
| floatText(`${label} +${pts}`,color,cleared===4||tspin?34:24); | |
| if(combo>0){floatText(`${combo} COMBO!`,"#39ff6a",20);SE.combo(combo);} | |
| if(grid.every(row=>row.every(c=>!c))){score+=3500*level;floatText("PERFECT CLEAR!! +"+3500*level,"#fff",30);SE.perfect();flash(0.8);spawnParticles(W/2,H/2,{n:200,power:600});} | |
| }else{combo=-1;} | |
| canHold=true; | |
| updateBanner();updateHUD(); | |
| spawnPiece(); | |
| } | |
| let clearAnim=null; | |
| function triggerClearFX(n,rows,label,color){ | |
| // 消去行の位置に爆発 | |
| for(const y of rows){ | |
| const sy=(y-HIDDEN)*CELL+CELL/2; | |
| spawnParticles(W/2,sy,{n:40+n*25,lines:n,power:350+n*90, | |
| colors:[color,"#ffffff","#ffe600",COLORS[cur.type]]}); | |
| } | |
| // 画面シェイク・フラッシュ強度 | |
| shake(Math.min(12,2+n*2.2)); | |
| flash(Math.min(0.75,0.15+n*0.14), n===4?"#ffe600":color, 100); | |
| bgHue=(bgHue+60)%360; | |
| updateBanner(true); | |
| } | |
| function updateBanner(pop=false){ | |
| const b=document.getElementById("effect-banner"); | |
| const t=tierOf(lines); | |
| b.textContent=`Lv.${t} ${TIERS[t]} ― ${lines} LINES`; | |
| if(pop){b.classList.add("pop");setTimeout(()=>b.classList.remove("pop"),180);} | |
| } | |
| // ---------- 描画 ---------- | |
| function drawCell(g,px,py,color,type){ | |
| const x=px*CELL,y=(py-HIDDEN)*CELL; | |
| if(y<-CELL||y>H)return; | |
| const grd=g.createLinearGradient(x,y,x,y+CELL); | |
| grd.addColorStop(0,color);grd.addColorStop(1,DARK[type]||"#333"); | |
| g.fillStyle=grd; | |
| g.fillRect(x+1,y+1,CELL-2,CELL-2); | |
| g.fillStyle="rgba(255,255,255,.45)"; | |
| g.fillRect(x+3,y+3,CELL-6,4); | |
| g.strokeStyle="rgba(255,255,255,.35)";g.strokeRect(x+1.5,y+1.5,CELL-3,CELL-3); | |
| } | |
| function ghostY(){ | |
| let y=cur.y;while(!collides(cur.matrix,cur.x,y+1))y++;return y; | |
| } | |
| function draw(){ | |
| ctx.clearRect(0,0,W,H); | |
| // グリッド | |
| ctx.strokeStyle="rgba(255,255,255,.06)";ctx.lineWidth=1; | |
| for(let x=1;x<COLS;x++){ctx.beginPath();ctx.moveTo(x*CELL,0);ctx.lineTo(x*CELL,H);ctx.stroke();} | |
| for(let y=1;y<ROWS;y++){ctx.beginPath();ctx.moveTo(0,y*CELL);ctx.lineTo(W,y*CELL);ctx.stroke();} | |
| // 固定ブロック | |
| for(let y=HIDDEN;y<ROWS+HIDDEN;y++)for(let x=0;x<COLS;x++){ | |
| const t=grid[y][x];if(t)drawCell(ctx,x,y,COLORS[t],t); | |
| } | |
| // 消去フラッシュ | |
| if(clearAnim){ | |
| ctx.fillStyle=`rgba(255,255,255,${Math.max(0,0.9-clearAnim.t*6)})`; | |
| for(const y of clearAnim.rows)ctx.fillRect(0,(y-HIDDEN)*CELL,W,CELL); | |
| } | |
| if(cur&&state!=="over"){ | |
| // ゴースト | |
| const gy=ghostY(); | |
| ctx.save();ctx.globalAlpha=0.28; | |
| for(const [x,y] of cellsOf(cur.matrix)){ | |
| const bx=cur.x+x,by=gy+y; | |
| ctx.strokeStyle=COLORS[cur.type];ctx.lineWidth=2; | |
| ctx.strokeRect(bx*CELL+2,(by)*CELL+2,CELL-4,CELL-4); | |
| } | |
| ctx.restore(); | |
| // 現ミノ (接地間近で点滅) | |
| const blink=onGround()?(Math.sin(performance.now()/60)>0?1:0.6):1; | |
| ctx.save();ctx.globalAlpha=blink; | |
| for(const [x,y] of cellsOf(cur.matrix))drawCell(ctx,cur.x+x,cur.y+y+HIDDEN,COLORS[cur.type],cur.type); | |
| ctx.restore(); | |
| } | |
| drawParticles(); | |
| } | |
| function drawMini(g,cv,type){ | |
| g.clearRect(0,0,cv.width,cv.height); | |
| if(!type)return; | |
| const m=SHAPES[type],cell=22; | |
| const ox=(cv.width-m[0].length*cell)/2, oy=(cv.height-m.length*cell)/2; | |
| for(let y=0;y<m.length;y++)for(let x=0;x<m[y].length;x++)if(m[y][x]){ | |
| const grd=g.createLinearGradient(0,oy+y*cell,0,oy+(y+1)*cell); | |
| grd.addColorStop(0,COLORS[type]);grd.addColorStop(1,DARK[type]); | |
| g.fillStyle=grd;g.fillRect(ox+x*cell+1,oy+y*cell+1,cell-2,cell-2); | |
| } | |
| } | |
| function drawHold(){drawMini(hctx,holdCv,holdPiece);} | |
| function drawNext(){ | |
| nctx.clearRect(0,0,nextCv.width,nextCv.height); | |
| queue.slice(0,5).forEach((t,i)=>{ | |
| nctx.save();nctx.translate(0,i*66);drawMini(nctx,{width:120,height:66},t);nctx.restore(); | |
| }); | |
| } | |
| // drawMini互換のため簡易ラッパ | |
| const _drawMini=drawMini; | |
| function updateHUD(){ | |
| document.getElementById("score").textContent=score.toLocaleString(); | |
| document.getElementById("lines").textContent=lines; | |
| document.getElementById("level").textContent=level; | |
| const c=document.getElementById("combo"); | |
| c.textContent=Math.max(0,combo); | |
| c.className=combo>=2?"combo-fire":""; | |
| document.getElementById("b2b").textContent=b2b>0?"🔥B2B x"+b2b:""; | |
| const s=Math.floor(elapsed/1000); | |
| document.getElementById("time").textContent=`${(s/60)|0}:${String(s%60).padStart(2,"0")}`; | |
| } | |
| // ---------- メインループ ---------- | |
| let raf=0,prevT=0; | |
| function loop(t){ | |
| raf=requestAnimationFrame(loop); | |
| const dt=Math.min(0.05,(t-prevT)/1000||0);prevT=t; | |
| drawBg(dt); | |
| if(state==="play"){ | |
| elapsed=performance.now()-startTime; | |
| handleDAS(dt); | |
| // 重力 | |
| const iv=(keys.has("down")?0.03:gravitySec(level)); | |
| dropCounter+=dt; | |
| if(dropCounter>=iv){ | |
| dropCounter=0; | |
| if(!collides(cur.matrix,cur.x,cur.y+1)){cur.y++;if(keys.has("down"))score+=1;lastAction="fall";lockTimer=0;} | |
| else{ | |
| lockTimer+=iv*1000; | |
| if(lockTimer>=LOCK_DELAY)lockPiece(); | |
| } | |
| }else if(onGround()){ | |
| lockTimer+=dt*1000; | |
| // ロックメーター表示のため再描画のみ | |
| if(lockTimer>=LOCK_DELAY)lockPiece(); | |
| }else lockTimer=0; | |
| if((t|0)%500<20)updateHUD(); | |
| } | |
| if(clearAnim){clearAnim.t+=dt;if(clearAnim.t>0.25)clearAnim=null;} | |
| updateParticles(dt); | |
| draw(); | |
| } | |
| // ---------- 入力 (DAS/ARR) ---------- | |
| const keys=new Set(); | |
| function handleDAS(dt){ | |
| let dir=0; | |
| if(keys.has("left"))dir=-1;else if(keys.has("right"))dir=1; | |
| if(dir!==dasDir){dasDir=dir;dasTimer=0;arrTimer=0; | |
| if(dir!==0)move(dir); | |
| }else if(dir!==0){ | |
| dasTimer+=dt*1000; | |
| if(dasTimer>=DAS){arrTimer+=dt*1000; | |
| while(arrTimer>=ARR){arrTimer-=ARR;if(!move(dir))break;} | |
| } | |
| } | |
| } | |
| const keymap={ArrowLeft:"left",ArrowRight:"right",ArrowDown:"down"}; | |
| addEventListener("keydown",e=>{ | |
| if(["ArrowLeft","ArrowRight","ArrowDown","ArrowUp"," "].includes(e.key))e.preventDefault(); | |
| if(e.repeat){if(keymap[e.key])keys.add(keymap[e.key]);return;} | |
| ac(); | |
| switch(e.key){ | |
| case "ArrowLeft":keys.add("left");break; | |
| case "ArrowRight":keys.add("right");break; | |
| case "ArrowDown":keys.add("down");break; | |
| case "ArrowUp":case "x":case "X":tryRotate(1);break; | |
| case "z":case "Z":tryRotate(-1);break; | |
| case "a":case "A":tryRotate(2);break; | |
| case " ":state==="play"?hardDrop():startGame();break; | |
| case "c":case "C":case "Shift":hold();break; | |
| case "p":case "P":togglePause();break; | |
| case "r":case "R":startGame();break; | |
| case "m":case "M":toggleMute();break; | |
| case "Enter":startGame();break; | |
| } | |
| }); | |
| addEventListener("keyup",e=>{if(keymap[e.key])keys.delete(keymap[e.key]);}); | |
| // タッチ | |
| document.querySelectorAll("#touch button").forEach(b=>{ | |
| const k=b.dataset.k; | |
| const down=e=>{e.preventDefault();ac(); | |
| if(k==="left"||k==="right"||k==="down")keys.add(k); | |
| else if(k==="rotR")tryRotate(1);else if(k==="rotL")tryRotate(-1); | |
| else if(k==="hard")hardDrop();else if(k==="hold")hold();}; | |
| const up=()=>{keys.delete(k);}; | |
| b.addEventListener("pointerdown",down); | |
| b.addEventListener("pointerup",up);b.addEventListener("pointerleave",up); | |
| }); | |
| // ---------- 開始/終了 ---------- | |
| function startGame(){ | |
| ac();reset();state="play"; | |
| document.getElementById("overlay").classList.add("hidden"); | |
| updateBanner();startBGM(); | |
| } | |
| function gameOver(){ | |
| state="over";stopBGM();SE.over(); | |
| shake(10);flash(0.7,"#ff2f4e",300); | |
| spawnParticles(W/2,H/2,{n:120,power:500,colors:["#ff2f4e","#fff","#ffe600"]}); | |
| const ov=document.getElementById("overlay"); | |
| ov.classList.remove("hidden"); | |
| document.getElementById("ov-title").textContent="GAME OVER"; | |
| ov.querySelector("p").innerHTML= | |
| `SCORE <b style="color:#ffe600">${score.toLocaleString()}</b> / ${lines} LINES / Lv.${level}<br>`+ | |
| `もう一度? <kbd>Space</kbd>/<kbd>R</kbd> でリスタート`; | |
| document.getElementById("btn-start").textContent="もう一回!!"; | |
| } | |
| function togglePause(){ | |
| if(state==="play"){state="pause"; | |
| document.getElementById("overlay").classList.remove("hidden"); | |
| document.getElementById("ov-title").textContent="⏸ PAUSE"; | |
| ovTextDefault(); | |
| }else if(state==="pause"){state="play"; | |
| document.getElementById("overlay").classList.add("hidden");} | |
| } | |
| function ovTextDefault(){ | |
| document.querySelector("#overlay p").innerHTML= | |
| `<kbd>P</kbd> で再開 / <kbd>R</kbd> でリスタート<br>10ラインごとにエフェクト進化!`; | |
| document.getElementById("btn-start").textContent="再開する"; | |
| } | |
| function toggleMute(){muted=!muted;document.getElementById("btn-mute").textContent=muted?"🔇 M":"🔊 M";} | |
| document.getElementById("btn-start").onclick=startGame; | |
| document.getElementById("btn-restart").onclick=startGame; | |
| document.getElementById("btn-pause").onclick=()=>{ | |
| if(state==="ready")startGame();else togglePause(); | |
| document.getElementById("btn-start").textContent=state==="play"?"再開する":"ゲーム開始!!"; | |
| }; | |
| document.getElementById("btn-mute").onclick=toggleMute; | |
| // 初期描画 | |
| reset();state="ready"; | |
| requestAnimationFrame(loop); | |
| </script> | |
| </body> | |
| </html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment