/**
 * qix_howtoplay.js
 * How To Play — スクリプト制御アニメーションデモ
 */
class HowToPlay {
    constructor() {
        this.demos = this._buildDemos();
        this.current = 0;
        this.t = 0;
        this._raf = null;
        this._canvas = null;
        this._ctx = null;
        this._onUpdate = null;
    }

    attach(canvas, onUpdate) {
        this._canvas = canvas;
        this._ctx = canvas.getContext('2d');
        this._onUpdate = onUpdate;
    }

    start() { this.t = 0; this._loop(); }

    stop() {
        if (this._raf) { cancelAnimationFrame(this._raf); this._raf = null; }
    }

    goto(i) {
        this.current = ((i % this.demos.length) + this.demos.length) % this.demos.length;
        this.t = 0;
        if (this._onUpdate) this._onUpdate();
    }

    next() { this.goto(this.current + 1); }
    prev() { this.goto(this.current - 1); }

    _loop() {
        const demo = this.demos[this.current];
        this.t = (this.t + 1) % demo.frames;
        const W = this._canvas.width, H = this._canvas.height;
        this._ctx.clearRect(0, 0, W, H);
        demo.draw(this._ctx, this.t, demo.frames, W, H);
        this._raf = requestAnimationFrame(() => this._loop());
    }

    _buildDemos() {
        const CS = 8, GW = 26, GH = 16;

        const gridBase = (W, H) => ({
            ox: Math.floor((W - GW * CS) / 2),
            oy: Math.floor((H - GH * CS) / 2),
        });

        const lerp  = (a, b, t) => a + (b - a) * t;
        const clamp = (v, lo, hi) => Math.max(lo, Math.min(hi, v));
        const ease  = t => t < 0.5 ? 2*t*t : -1+(4-2*t)*t;

        function drawGrid(ctx, W, H, claimed = new Set(), trail = new Set()) {
            const { ox, oy } = gridBase(W, H);
            for (let y = 0; y < GH; y++) {
                for (let x = 0; x < GW; x++) {
                    const idx = y * GW + x;
                    const wall = x===0||x===GW-1||y===0||y===GH-1;
                    let c;
                    if (wall)             c = '#1e0830';
                    else if (claimed.has(idx)) c = 'rgba(188,19,254,0.22)';
                    else if (trail.has(idx))   c = 'rgba(255,140,0,0.75)';
                    else                  c = '#070010';
                    ctx.fillStyle = c;
                    ctx.fillRect(ox + x*CS, oy + y*CS, CS-1, CS-1);
                }
            }
            ctx.strokeStyle = 'rgba(188,19,254,0.45)';
            ctx.lineWidth = 1;
            ctx.strokeRect(ox + 0.5, oy + 0.5, GW*CS - 1, GH*CS - 1);
        }

        function dot(ctx, W, H, gx, gy, r, color) {
            const { ox, oy } = gridBase(W, H);
            ctx.beginPath();
            ctx.arc(ox + gx*CS + CS/2, oy + gy*CS + CS/2, r, 0, Math.PI*2);
            ctx.fillStyle = color;
            ctx.fill();
        }

        function label(ctx, text, x, y, color='rgba(188,19,254,0.85)', size=9) {
            ctx.fillStyle = color;
            ctx.font = `${size}px monospace`;
            ctx.fillText(text, x, y);
        }

        return [
            // ① 移動
            {
                title: '① 移動',
                desc: '壁の上を移動してスタート。矢印キーまたはスティックで操作します。',
                frames: 160,
                draw(ctx, t, F, W, H) {
                    const p = t / F;
                    drawGrid(ctx, W, H);
                    let gx, gy;
                    if (p < 0.5) { gx = lerp(2, 23, ease(p/0.5)); gy = 0; }
                    else         { gx = 23; gy = lerp(0, 14, ease((p-0.5)/0.5)); }
                    dot(ctx, W, H, gx, gy, 3.5, '#fff');
                    const { ox, oy } = gridBase(W, H);
                    ctx.fillStyle = 'rgba(188,19,254,0.5)';
                    ctx.font = '8px monospace';
                    ctx.fillText('WALL', ox + 4, oy - 3);
                },
            },
            // ② 囲み
            {
                title: '② 領域を囲む',
                desc: '壁を離れて軌跡を引き、壁に戻ると領域が確保されます。',
                frames: 220,
                draw(ctx, t, F, W, H) {
                    const p = t / F;
                    const trail = new Set();
                    const claimed = new Set();
                    const { ox, oy } = gridBase(W, H);
                    const sx=7, ex=18, my=10;

                    let gx=sx, gy=0;

                    if (p < 0.2) {
                        gx = lerp(2, sx, ease(p/0.2)); gy = 0;
                    } else if (p < 0.38) {
                        gx = sx; gy = lerp(0, my, ease((p-0.2)/0.18));
                        for(let i=1;i<=Math.floor(gy);i++) trail.add(i*GW+sx);
                    } else if (p < 0.55) {
                        gx = lerp(sx, ex, ease((p-0.38)/0.17)); gy = my;
                        for(let i=1;i<=my;i++) trail.add(i*GW+sx);
                        for(let i=sx;i<=Math.floor(gx);i++) trail.add(my*GW+i);
                    } else if (p < 0.72) {
                        gx = ex; gy = lerp(my, 0, ease((p-0.55)/0.17));
                        for(let i=1;i<=my;i++) trail.add(i*GW+sx);
                        for(let i=sx;i<=ex;i++) trail.add(my*GW+i);
                        for(let i=0;i<=Math.floor(my-gy);i++) trail.add((my-i)*GW+ex);
                    } else {
                        gx = ex; gy = 0;
                        const fp = Math.min((p-0.72)/0.28, 1);
                        const cols = Math.floor(fp*(ex-sx-1));
                        for(let cy=1;cy<my;cy++) for(let cx=sx+1;cx<=sx+cols;cx++) claimed.add(cy*GW+cx);
                    }

                    drawGrid(ctx, W, H, claimed, trail);
                    if (p < 0.72 || claimed.size===0) dot(ctx, W, H, gx, gy, 3.5, '#fff');

                    if (claimed.size > 0) {
                        ctx.fillStyle = `rgba(0,255,180,${Math.sin((p-0.72)*Math.PI*5)*0.4+0.4})`;
                        ctx.font = 'bold 11px monospace';
                        ctx.textAlign = 'center';
                        ctx.fillText('CLAIMED!', W/2, oy - 5);
                        ctx.textAlign = 'left';
                    }
                },
            },
            // ③ 敵を囲む
            {
                title: '③ 敵を囲んで撃破',
                desc: '囲んだ領域の中に敵がいると、そのまま撃破できます。',
                frames: 240,
                draw(ctx, t, F, W, H) {
                    const p = t / F;
                    const trail = new Set();
                    const claimed = new Set();
                    const { ox, oy } = gridBase(W, H);
                    const sx=7, ex=19, my=11;

                    const ex_pos = 13 + Math.sin(p*Math.PI*6)*2.5;
                    const ey_pos =  6 + Math.cos(p*Math.PI*4)*2;

                    let gx=sx, gy=0, phase='move';

                    if (p < 0.18)      { gx=lerp(2,sx,ease(p/0.18)); gy=0; phase='wall'; }
                    else if (p < 0.36) { gx=sx; gy=lerp(0,my,ease((p-0.18)/0.18)); phase='down'; for(let i=1;i<=Math.floor(gy);i++) trail.add(i*GW+sx); }
                    else if (p < 0.54) { gx=lerp(sx,ex,ease((p-0.36)/0.18)); gy=my; phase='right'; for(let i=1;i<=my;i++) trail.add(i*GW+sx); for(let i=sx;i<=Math.floor(gx);i++) trail.add(my*GW+i); }
                    else if (p < 0.70) { gx=ex; gy=lerp(my,0,ease((p-0.54)/0.16)); phase='up'; for(let i=1;i<=my;i++) trail.add(i*GW+sx); for(let i=sx;i<=ex;i++) trail.add(my*GW+i); for(let i=0;i<=Math.floor(my-gy);i++) trail.add((my-i)*GW+ex); }
                    else {
                        gx=ex; gy=0; phase='fill';
                        const fp = Math.min((p-0.70)/0.15, 1);
                        const cols = Math.floor(fp*(ex-sx-1));
                        for(let cy=1;cy<my;cy++) for(let cx=sx+1;cx<=sx+cols;cx++) claimed.add(cy*GW+cx);
                    }

                    drawGrid(ctx, W, H, claimed, trail);
                    if (phase !== 'fill') dot(ctx, W, H, gx, gy, 3.5, '#fff');

                    const trapped = claimed.size > 0;
                    if (p < 0.85 || !trapped) {
                        const flash = trapped && Math.sin(p*90) > 0;
                        if (!flash) dot(ctx, W, H, ex_pos, ey_pos, 3.5, '#FF0055');
                    } else {
                        const ep = (p-0.85)/0.15;
                        const ex2 = ox+(ex_pos+0.5)*CS, ey2 = oy+(ey_pos+0.5)*CS;
                        for(let i=0;i<6;i++){
                            const a=i/6*Math.PI*2, r=ep*22;
                            ctx.beginPath(); ctx.arc(ex2+Math.cos(a)*r, ey2+Math.sin(a)*r, 2*(1-ep),0,Math.PI*2);
                            ctx.fillStyle=`rgba(255,0,85,${1-ep})`; ctx.fill();
                        }
                    }
                },
            },
            // ④ 撃破ボーナス
            {
                title: '④ 撃破ボーナス',
                desc: '敵を囲んで倒すとMPを回収。強い敵ほどボーナスが大きい。',
                frames: 200,
                draw(ctx, t, F, W, H) {
                    const p = t / F;
                    const { ox, oy } = gridBase(W, H);

                    // Phase: 0-0.45 囲み中 / 0.45-0.65 爆発 / 0.65-1.0 MP回収
                    const claimed = new Set();
                    const trail = new Set();
                    const ex=13, ey=8;

                    if (p < 0.45) {
                        const fp = p / 0.45;
                        const cols = Math.floor(fp * 8);
                        for(let cy=4;cy<=11;cy++) for(let cx=8;cx<=8+cols;cx++) claimed.add(cy*GW+cx);
                        if(fp > 0.5) for(let cy=4;cy<=11;cy++) trail.add(cy*GW+16);
                        drawGrid(ctx, W, H, claimed, trail);
                        dot(ctx, W, H, ex, ey, 3.5, '#FF0055');
                        dot(ctx, W, H, fp<0.5?8:16, fp<0.5?lerp(4,8,fp*2):8, 3.5, '#fff');
                    } else if (p < 0.65) {
                        for(let cy=4;cy<=11;cy++) for(let cx=8;cx<=16;cx++) claimed.add(cy*GW+cx);
                        drawGrid(ctx, W, H, claimed, trail);
                        const ep = (p-0.45)/0.2;
                        const cx2=ox+(ex+0.5)*CS, cy2=oy+(ey+0.5)*CS;
                        for(let i=0;i<8;i++){
                            const a=i/8*Math.PI*2, r=ep*3.5*CS;
                            ctx.beginPath(); ctx.arc(cx2+Math.cos(a)*r, cy2+Math.sin(a)*r, 2.5*(1-ep),0,Math.PI*2);
                            ctx.fillStyle=`rgba(255,80,0,${1-ep})`; ctx.fill();
                        }
                        ctx.fillStyle=`rgba(0,255,80,${1-ep})`;
                        ctx.font='bold 11px monospace'; ctx.textAlign='center';
                        ctx.fillText('MP +20', W/2, H/2-10);
                        ctx.textAlign='left';
                    } else {
                        for(let cy=4;cy<=11;cy++) for(let cx=8;cx<=16;cx++) claimed.add(cy*GW+cx);
                        drawGrid(ctx, W, H, claimed, trail);
                        dot(ctx, W, H, 16, 8, 3.5, '#fff');

                        const rp = (p-0.65)/0.35;
                        const mpw=80, mph=8, mpx=(W-mpw)/2, mpy=H-24;
                        ctx.fillStyle='#220022'; ctx.fillRect(mpx,mpy,mpw,mph);
                        const fillTo = clamp(0.4 + rp*0.35, 0, 1);
                        ctx.fillStyle='rgba(188,19,254,0.85)'; ctx.fillRect(mpx,mpy,mpw*fillTo,mph);

                        const floatY = oy+(ey+0.5)*CS + rp*(mpy - oy-(ey+0.5)*CS);
                        ctx.fillStyle=`rgba(0,255,120,${1-rp*0.5})`;
                        ctx.font='bold 10px monospace'; ctx.textAlign='center';
                        ctx.fillText('MP +20', W/2, floatY);
                        ctx.textAlign='left';
                        ctx.fillStyle='#BC13FE'; ctx.font='7px monospace';
                        ctx.fillText('MP', mpx-14, mpy+7);
                    }
                },
            },
            // ⑤ ダメージ・リワインド
            {
                title: '⑤ ダメージ＆リワインド',
                desc: '軌跡や自機に敵が当たると爆発。引いた軌跡がMP消費で巻き戻される。MPが足りないとゲームオーバー。',
                frames: 260,
                draw(ctx, t, F, W, H) {
                    const p = t / F;
                    const { ox, oy } = gridBase(W, H);

                    const claimed = new Set();
                    for(let gy=1;gy<GH-1;gy++) for(let gx=1;gx<10;gx++) claimed.add(gy*GW+gx);

                    const trailCells = [];
                    for(let i=10;i<=20;i++) trailCells.push({x:i, y:8});

                    const ex = 16 + Math.sin(p * Math.PI * 4) * 3;
                    const ey = 3 + p * 14;

                    if (p < 0.28) {
                        const trail = new Set(trailCells.map(c => c.y*GW+c.x));
                        drawGrid(ctx, W, H, claimed, trail);
                        dot(ctx, W, H, 20, 8, 3.5, '#fff');
                        dot(ctx, W, H, ex, ey, 3.5, '#FF0055');

                        const bw=70, bh=7, bx=W-bw-8, by=8;
                        ctx.fillStyle='#220022'; ctx.fillRect(bx,by,bw,bh);
                        ctx.fillStyle='rgba(188,19,254,0.85)'; ctx.fillRect(bx,by,bw*0.85,bh);
                        label(ctx,'MP',bx-14,by+6,'#BC13FE',7);

                    } else if (p < 0.42) {
                        const trail = new Set(trailCells.map(c => c.y*GW+c.x));
                        drawGrid(ctx, W, H, claimed, trail);

                        const ep = (p-0.28)/0.14;
                        const hx = ox+(16+0.5)*CS, hy = oy+(8+0.5)*CS;
                        ctx.beginPath(); ctx.arc(hx,hy, ep*3*CS, 0, Math.PI*2);
                        ctx.strokeStyle=`rgba(255,50,0,${1-ep})`; ctx.lineWidth=2; ctx.stroke();
                        ctx.fillStyle=`rgba(255,150,0,${(1-ep)*0.5})`;
                        ctx.beginPath(); ctx.arc(hx,hy, ep*2*CS, 0, Math.PI*2); ctx.fill();

                        label(ctx,'HIT!', hx-14, hy-14, `rgba(255,80,0,${1-ep*0.5})`, 12);

                        const bw=70, bh=7, bx=W-bw-8, by=8;
                        ctx.fillStyle='#220022'; ctx.fillRect(bx,by,bw,bh);
                        ctx.fillStyle='rgba(188,19,254,0.85)'; ctx.fillRect(bx,by,bw*0.85,bh);
                        label(ctx,'MP',bx-14,by+6,'#BC13FE',7);

                    } else if (p < 0.62) {
                        const rp = (p-0.42)/0.20;
                        const remaining = Math.floor((1-rp) * trailCells.length);
                        const trail = new Set(trailCells.slice(0, remaining).map(c => c.y*GW+c.x));
                        drawGrid(ctx, W, H, claimed, trail);

                        if(remaining > 0) dot(ctx, W, H, trailCells[remaining-1].x, 8, 3.5, '#00ffff');
                        else dot(ctx, W, H, 10, 8, 3.5, '#00ffff');

                        const mpLeft = 0.85 - rp * 0.35;
                        const bw=70, bh=7, bx=W-bw-8, by=8;
                        ctx.fillStyle='#220022'; ctx.fillRect(bx,by,bw,bh);
                        ctx.fillStyle='rgba(188,19,254,0.85)'; ctx.fillRect(bx,by,bw*mpLeft,bh);
                        label(ctx,'MP',bx-14,by+6,'#BC13FE',7);

                        const flash = Math.sin(p*Math.PI*18) > 0;
                        label(ctx,'REWIND...', W/2-24, H-14, flash?'rgba(0,200,255,0.9)':'rgba(0,200,255,0.35)', 9);

                    } else {
                        const rp = (p-0.62)/0.38;
                        drawGrid(ctx, W, H, claimed);
                        dot(ctx, W, H, 10, 8, 3.5, `rgba(255,255,255,${0.4+Math.sin(rp*Math.PI*6)*0.4})`);

                        const bw=70, bh=7, bx=W-bw-8, by=8;
                        ctx.fillStyle='#220022'; ctx.fillRect(bx,by,bw,bh);
                        ctx.fillStyle='rgba(188,19,254,0.85)'; ctx.fillRect(bx,by,bw*0.50,bh);
                        label(ctx,'MP',bx-14,by+6,'#BC13FE',7);

                        label(ctx,'壁に戻る', W/2-20, H-14, `rgba(180,180,180,${rp*0.8})`, 9);
                    }
                },
            },
            // ⑥ クリア条件
            {
                title: '⑥ クリア条件',
                desc: 'ERODED率がTARGETに達するか、全敵を倒すとステージクリア！ただし敵同士の同士討ちは倒したと見なさない。',
                frames: 280,
                draw(ctx, t, F, W, H) {
                    const p = t / F;
                    const { ox, oy } = gridBase(W, H);

                    if (p < 0.5) {
                        // --- フェーズ1: ERODED達成クリア ---
                        const lp = p / 0.5;
                        const fillAmt = clamp(lp * 1.4, 0, 0.9);
                        const claimed = new Set();
                        const total = (GW-2)*(GH-2);
                        const cnt = Math.floor(fillAmt * total);
                        let n=0;
                        outer: for(let gy=1;gy<GH-1;gy++) for(let gx=1;gx<GW-1;gx++){if(n>=cnt) break outer; claimed.add(gy*GW+gx); n++;}
                        drawGrid(ctx, W, H, claimed);

                        const bx=16, by=H-26, bw=W-32, bh=11;
                        ctx.fillStyle='#110018'; ctx.fillRect(bx,by,bw,bh);
                        ctx.fillStyle='rgba(188,19,254,0.85)'; ctx.fillRect(bx,by,bw*fillAmt,bh);
                        const tx = bx + bw*0.75;
                        ctx.strokeStyle='#FF0055'; ctx.lineWidth=1.5;
                        ctx.beginPath(); ctx.moveTo(tx,by-4); ctx.lineTo(tx,by+bh+4); ctx.stroke();
                        ctx.fillStyle='#aaa'; ctx.font='8px monospace';
                        ctx.fillText(`ERODED: ${(fillAmt*100).toFixed(0)}%`, bx, by-4);
                        ctx.fillStyle='#FF0055'; ctx.fillText('TARGET', tx-1, by+bh+11);

                        if (fillAmt >= 0.75) {
                            const alpha = Math.sin((lp-0.54)*Math.PI*8)*0.5+0.5;
                            ctx.fillStyle=`rgba(0,255,180,${alpha})`;
                            ctx.font='bold 16px monospace'; ctx.textAlign='center';
                            ctx.fillText('CLEAR!', W/2, oy+4);
                            ctx.textAlign='left';
                        }
                        label(ctx,'条件①: ERODED達成', 8, 12, 'rgba(188,19,254,0.7)', 8);
                    } else {
                        // --- フェーズ2: 全敵撃破クリア ---
                        const lp = (p - 0.5) / 0.5;
                        // 4体の敵が順番に消えていく
                        const enemyCount = 4;
                        const alive = Math.max(0, Math.ceil((1 - lp * 1.3) * enemyCount));
                        const positions = [{gx:8,gy:5},{gx:17,gy:5},{gx:8,gy:11},{gx:17,gy:11}];
                        drawGrid(ctx, W, H);

                        positions.forEach((pos, i) => {
                            if (i >= alive) {
                                // 爆発エフェクト
                                const ep = clamp((lp * 1.3 - (enemyCount - i - 1) / enemyCount) * enemyCount, 0, 1);
                                if (ep > 0 && ep < 1) {
                                    const cx2 = ox + pos.gx * CS + CS/2;
                                    const cy2 = oy + pos.gy * CS + CS/2;
                                    for(let k=0;k<5;k++){
                                        const a = k/5*Math.PI*2;
                                        ctx.beginPath(); ctx.arc(cx2+Math.cos(a)*ep*14, cy2+Math.sin(a)*ep*14, 2*(1-ep), 0, Math.PI*2);
                                        ctx.fillStyle=`rgba(255,80,0,${1-ep})`; ctx.fill();
                                    }
                                }
                            } else {
                                dot(ctx, W, H, pos.gx, pos.gy, 4, '#FF0055');
                            }
                        });

                        if (alive === 0) {
                            const alpha = Math.sin(lp * Math.PI * 8) * 0.5 + 0.5;
                            ctx.fillStyle=`rgba(0,255,180,${alpha})`;
                            ctx.font='bold 16px monospace'; ctx.textAlign='center';
                            ctx.fillText('CLEAR!', W/2, H/2);
                            ctx.textAlign='left';
                        }
                        label(ctx,'条件②: 全敵撃破', 8, 12, 'rgba(255,100,0,0.7)', 8);
                    }
                },
            },
            // ⑦ スパーク
            {
                title: '⑦ スパーク警戒',
                desc: '領域が40%を超えると壁面を走るスパークが出現。接触すると軌跡を失う。',
                frames: 220,
                draw(ctx, t, F, W, H) {
                    const p = t / F;
                    const { ox, oy } = gridBase(W, H);

                    const claimed = new Set();
                    for(let gy=1;gy<GH-1;gy++) for(let gx=1;gx<13;gx++) claimed.add(gy*GW+gx);

                    const wallRoute = [];
                    for(let y=0;y<GH;y++) wallRoute.push({x:13,y});
                    for(let x=13;x>=0;x--) wallRoute.push({x,y:0});
                    for(let y=0;y<GH;y++) wallRoute.push({x:0,y});
                    for(let x=0;x<=13;x++) wallRoute.push({x,y:GH-1});

                    drawGrid(ctx, W, H, claimed);

                    const trail = new Set();
                    const px = 16, py = 8;
                    for(let i=14;i<=px;i++) trail.add(8*GW+i);
                    for(const idx of trail){
                        const tx=idx%GW, ty=Math.floor(idx/GW);
                        ctx.fillStyle='rgba(255,140,0,0.75)';
                        ctx.fillRect(ox+tx*CS, oy+ty*CS, CS-1, CS-1);
                    }
                    dot(ctx, W, H, px, py, 3.5, '#fff');

                    const routeLen = wallRoute.length;
                    [0, 0.5].forEach((offset, i) => {
                        const sp = (p + offset) % 1;
                        const ri = Math.floor(sp * routeLen);
                        const pos = wallRoute[ri];
                        const alpha = 0.6 + Math.sin(sp * Math.PI * 20) * 0.35;
                        const sx = ox + pos.x * CS + CS/2;
                        const sy = oy + pos.y * CS + CS/2;
                        ctx.beginPath(); ctx.arc(sx, sy, 3.5, 0, Math.PI*2);
                        ctx.fillStyle = `rgba(255,220,0,${alpha})`; ctx.fill();
                        ctx.beginPath(); ctx.arc(sx, sy, 5.5, 0, Math.PI*2);
                        ctx.strokeStyle = `rgba(255,200,0,${alpha*0.5})`; ctx.lineWidth=1; ctx.stroke();
                    });

                    const warn = p > 0.1;
                    label(ctx, '40%超えで出現', ox+GW*CS/2-20, oy-4,
                          warn ? 'rgba(255,200,0,0.75)' : 'rgba(255,200,0,0.2)', 8);
                },
            },
            // ⑧ ブースト
            {
                title: '⑧ ブースト',
                desc: '一時的に高速移動。軌跡コストが大きいので残MPに注意。',
                frames: 140,
                draw(ctx, t, F, W, H) {
                    const p = t / F;
                    const { ox, oy } = gridBase(W, H);
                    const trail = new Set();
                    const steps = Math.floor(p * 20);
                    for(let i=1;i<=steps&&i<21;i++) trail.add(8*GW+i+2);
                    drawGrid(ctx, W, H, new Set(), trail);

                    const px = clamp(3 + steps, 3, 22);
                    dot(ctx, W, H, px, 8, 3.5, '#00ffcc');

                    ctx.strokeStyle='rgba(0,255,200,0.25)'; ctx.lineWidth=1;
                    for(let i=-2;i<=2;i++){
                        const ly = oy+(8+i*0.7)*CS+CS/2;
                        const lx = ox+px*CS;
                        ctx.beginPath(); ctx.moveTo(lx-1,ly); ctx.lineTo(lx-18,ly); ctx.stroke();
                    }

                    const bw=52, bh=22, bx=W-bw-8, by=H-bh-8;
                    ctx.fillStyle='rgba(255,0,85,0.35)'; ctx.strokeStyle='#FF0055'; ctx.lineWidth=1;
                    ctx.beginPath(); ctx.roundRect(bx,by,bw,bh,3); ctx.fill(); ctx.stroke();
                    ctx.fillStyle='#fff'; ctx.font='bold 9px monospace'; ctx.textAlign='center';
                    ctx.fillText('BOOST', bx+bw/2, by+bh/2+3); ctx.textAlign='left';
                },
            },
            // ⑨ スロー
            {
                title: '⑨ スロー',
                desc: 'SLOWでゆっくり精密に動けます。MP消費あり。',
                frames: 160,
                draw(ctx, t, F, W, H) {
                    const p = t / F;
                    const { ox, oy } = gridBase(W, H);
                    const trail = new Set();
                    const steps = Math.floor(p * 10);
                    for(let i=0;i<steps;i++){
                        const gx=6+Math.floor(i/2), gy=6+(i%2===0?0:1);
                        trail.add(gy*GW+gx);
                    }
                    drawGrid(ctx, W, H, new Set(), trail);
                    ctx.fillStyle='rgba(0,80,220,0.07)'; ctx.fillRect(0,0,W,H);

                    const px=6+Math.floor(steps/2), py=6+(steps%2===0?0:1);
                    dot(ctx, W, H, Math.min(px,17), py, 3.5, '#88ccff');

                    const bw=52, bh=22, bx=W-bw-8, by=H-bh-8;
                    ctx.fillStyle='rgba(0,80,200,0.4)'; ctx.strokeStyle='#44aaff'; ctx.lineWidth=1;
                    ctx.beginPath(); ctx.roundRect(bx,by,bw,bh,3); ctx.fill(); ctx.stroke();
                    ctx.fillStyle='#fff'; ctx.font='bold 9px monospace'; ctx.textAlign='center';
                    ctx.fillText('SLOW', bx+bw/2, by+bh/2+3); ctx.textAlign='left';

                    const mpw=50, mph=6, mpx=8, mpy=H-16;
                    ctx.fillStyle='#220022'; ctx.fillRect(mpx,mpy,mpw,mph);
                    ctx.fillStyle='rgba(188,19,254,0.8)'; ctx.fillRect(mpx,mpy,mpw*(1-p*0.6),mph);
                    label(ctx,'MP',mpx,mpy-2,'#BC13FE',7);
                },
            },
            // ⑩ 地雷
            {
                title: '⑩ 地雷',
                desc: '地雷に近づきすぎると爆発！囲んで安全に除去しよう。',
                frames: 200,
                draw(ctx, t, F, W, H) {
                    const p = t / F;
                    const { ox, oy } = gridBase(W, H);
                    const mx=13, my=8;

                    drawGrid(ctx, W, H);

                    if (p < 0.55) {
                        const px = lerp(3, 11.5, ease(clamp(p/0.45,0,1)));
                        dot(ctx, W, H, px, 8, 3.5, '#fff');

                        const pulse = 0.65 + Math.sin(p*Math.PI*10)*0.35;
                        dot(ctx, W, H, mx, my, 4.5*pulse, `rgba(255,100,0,${pulse})`);

                        ctx.beginPath();
                        ctx.arc(ox+mx*CS+CS/2, oy+my*CS+CS/2, 3.2*CS, 0, Math.PI*2);
                        ctx.strokeStyle='rgba(255,100,0,0.18)'; ctx.lineWidth=1.5; ctx.stroke();

                        label(ctx,'danger zone','',0,'rgba(255,100,0,0)',7);
                    } else {
                        const ep = (p-0.55)/0.45;
                        const cx2=ox+(mx+0.5)*CS, cy2=oy+(my+0.5)*CS;
                        ctx.fillStyle=`rgba(255,150,0,${Math.max(0,0.7-ep)})`;
                        ctx.beginPath(); ctx.arc(cx2,cy2,ep*3*CS,0,Math.PI*2); ctx.fill();
                        for(let i=0;i<8;i++){
                            const a=i/8*Math.PI*2, r=ep*4*CS;
                            ctx.beginPath(); ctx.arc(cx2+Math.cos(a)*r,cy2+Math.sin(a)*r,2.5*(1-ep),0,Math.PI*2);
                            ctx.fillStyle=`rgba(255,${80+Math.floor(ep*120)},0,${1-ep})`; ctx.fill();
                        }
                        if (ep>0.3){
                            label(ctx,'BOOM!', W/2-18, H/2, `rgba(255,150,0,${Math.max(0,(0.8-ep)*2.5)})`, 14);
                        }
                    }
                },
            },
        ];
    }
}
