/**
 * qix_achievements.js
 * 実績システム — 100実績 (入門15/標準20/熟練25/上級33/伝説7)
 */

const ACHIEVEMENTS = [
    // ===== 入門 (15) =====
    { id:'first_clear',      cat:'入門', name:'とりあえず一個',     desc:'任意のステージをクリアした' },
    { id:'damage_first',     cat:'入門', name:'洗礼',               desc:'初めてダメージを受けた' },
    { id:'gameover_first',   cat:'入門', name:'記念すべき日',       desc:'初めてゲームオーバーになった' },
    { id:'first_trap',       cat:'入門', name:'囲んだった',         desc:'敵を初めて囲んで撃破した' },
    { id:'play_10',          cat:'入門', name:'常連さん',           desc:'累計10回プレイした',           prog:{stat:'totalPlays',     target:10}   },
    { id:'play_30',          cat:'入門', name:'習慣化',             desc:'累計30回プレイした',           prog:{stat:'totalPlays',     target:30}   },
    { id:'clear_3',          cat:'入門', name:'三歩目',             desc:'3種類のステージをクリアした' },
    { id:'clear_5',          cat:'入門', name:'始まりの5歩',        desc:'5種類のステージをクリアした' },
    { id:'erosion_70',       cat:'入門', name:'七割',               desc:'70%以上の侵食率でクリア' },
    { id:'beat_mine_stage',  cat:'入門', name:'爆発物処理班',       desc:'地雷(MINE)がいるステージをクリア' },
    { id:'beat_beamboss',    cat:'入門', name:'ビーム怖くない',     desc:'ビーム砲(BEAM_BOSS)がいるステージをクリア' },
    { id:'gallery_view',     cat:'入門', name:'鑑賞眼',             desc:'ギャラリーを開いた' },
    { id:'trap_qix',         cat:'入門', name:'最初の仕留め',       desc:'QIXを囲んで撃破した' },
    { id:'first_boost',      cat:'入門', name:'後押し',             desc:'ブーストを初めて使った' },
    { id:'creator_made',     cat:'入門', name:'クリエイター誕生',   desc:'自作ステージを初めてスロットに保存した' },

    // ===== 標準 (20) =====
    { id:'play_100',         cat:'標準', name:'三桁の壁',           desc:'累計100回プレイした',          prog:{stat:'totalPlays',     target:100}  },
    { id:'clear_10',         cat:'標準', name:'二桁の扉',           desc:'10種類のステージをクリアした' },
    { id:'clear_20',         cat:'標準', name:'慣れてきた',         desc:'20種類のステージをクリアした' },
    { id:'erosion_80',       cat:'標準', name:'まあ及第点',         desc:'80%以上の侵食率でクリア' },
    { id:'erosion_85',       cat:'標準', name:'丁寧な仕事',         desc:'85%以上の侵食率でクリア' },
    { id:'gameover_10',      cat:'標準', name:'よくあること',       desc:'10回ゲームオーバーになった',   prog:{stat:'totalGameovers', target:10}   },
    { id:'creator_clear',    cat:'標準', name:'自作攻略',           desc:'自分で作ったステージをクリアした' },
    { id:'low_clear_60',     cat:'標準', name:'効率主義',           desc:'侵食率60%以下でクリアした' },
    { id:'friendly_fire',    cat:'標準', name:'内輪もめ',           desc:'敵の弾が別の敵を撃破した',     hidden:true },
    { id:'double_trap',      cat:'標準', name:'二羽の鳥',           desc:'1回の囲みで2体以上を同時に撃破した' },
    { id:'big_20',           cat:'標準', name:'一網打尽',           desc:'1回の囲みで領域を20%以上一気に増やした' },
    { id:'tough',            cat:'標準', name:'貧乏性',             desc:'MPが30以下になりながらもクリアした' },
    { id:'no_boost_clear',   cat:'標準', name:'素朴な生き方',       desc:'ブーストを一度も使わずにクリアした' },
    { id:'trap_ghost',       cat:'標準', name:'成仏しな',           desc:'幽霊(GHOST)を囲んで撃破した' },
    { id:'trap_shooter',     cat:'標準', name:'弾幕を断つ',         desc:'射撃体(SHOOTER)を囲んで撃破した' },
    { id:'trap_tracker',     cat:'標準', name:'追跡者の終焉',       desc:'追跡体(TRACKER)を囲んで撃破した' },
    { id:'trap_snake',       cat:'標準', name:'ヘビは嫌いだ',       desc:'免疫体(SNAKE)を囲んで撃破した' },
    { id:'trap_10',          cat:'標準', name:'まだまだこれから',   desc:'累計10体の敵を囲んで撃破した',  prog:{stat:'totalTraps',     target:10}   },
    { id:'trap_50',          cat:'標準', name:'半人前',             desc:'累計50体の敵を囲んで撃破した',  prog:{stat:'totalTraps',     target:50}   },
    { id:'beat_sniper_stage',cat:'標準', name:'照準外し',           desc:'狙撃ビーム(SNIPER_BEAM)がいるステージをクリア' },

    // ===== 熟練 (25) =====
    { id:'play_300',         cat:'熟練', name:'体に染みついた',     desc:'累計300回プレイした',          prog:{stat:'totalPlays',     target:300}  },
    { id:'boost_nodamage',   cat:'熟練', name:'完璧な加速',         desc:'ブーストを使用してノーダメージでクリアした' },
    { id:'clear_30',         cat:'熟練', name:'三十路の壁',         desc:'30種類のステージをクリアした' },
    { id:'clear_50',         cat:'熟練', name:'折り返し点',         desc:'50種類のステージをクリアした' },
    { id:'erosion_90',       cat:'熟練', name:'几帳面め',           desc:'90%以上の侵食率でクリア' },
    { id:'erosion_92',       cat:'熟練', name:'こだわり派',         desc:'92%以上の侵食率でクリア' },
    { id:'gameover_30',      cat:'熟練', name:'何度も転んで',       desc:'30回ゲームオーバーになった',   prog:{stat:'totalGameovers', target:30}   },
    { id:'gameover_50',      cat:'熟練', name:'呪われし者',         desc:'50回ゲームオーバーになった',   prog:{stat:'totalGameovers', target:50}   },
    { id:'low_clear_40',     cat:'熟練', name:'最小侵食',           desc:'侵食率40%以下でクリアした' },
    { id:'triple_trap',      cat:'熟練', name:'鬼畜',               desc:'1回の囲みで3体以上を同時に撃破した' },
    { id:'four_trap',        cat:'熟練', name:'大量捕獲',           desc:'1回の囲みで4体以上を同時に撃破した' },
    { id:'big_25',           cat:'熟練', name:'食欲旺盛',           desc:'1回の囲みで領域を25%以上一気に増やした' },
    { id:'big_30',           cat:'熟練', name:'欲張り',             desc:'1回の囲みで領域を30%以上一気に増やした' },
    { id:'big_35',           cat:'熟練', name:'底なしの欲',         desc:'1回の囲みで領域を35%以上一気に増やした' },
    { id:'trap_beamboss',    cat:'熟練', name:'砲台制圧',           desc:'ビーム砲(BEAM_BOSS)を囲んで撃破した' },
    { id:'trap_mine',        cat:'熟練', name:'爆発物制圧',         desc:'地雷(MINE)を囲んで撃破した' },
    { id:'trap_100',         cat:'熟練', name:'虐殺術師',           desc:'累計100体の敵を囲んで撃破した', prog:{stat:'totalTraps',     target:100}  },
    { id:'trap_200',         cat:'熟練', name:'中堅どころ',         desc:'累計200体の敵を囲んで撃破した', prog:{stat:'totalTraps',     target:200}  },
    { id:'no_damage',        cat:'熟練', name:'触れてすらいない',   desc:'一度もダメージを受けずにクリア' },
    { id:'low_mp_clear',     cat:'熟練', name:'心臓に悪い',         desc:'残MP10以下でクリアした' },
    { id:'mine_sweep_3',     cat:'熟練', name:'地雷コレクター',     desc:'同一ステージで地雷を3個以上トラップで除去した' },
    { id:'trail_5000',       cat:'熟練', name:'長い軌跡',           desc:'累計で引いた軌跡が10万マス以上', prog:{stat:'totalTrailCells',target:100000} },
    { id:'sparks_death_5',   cat:'熟練', name:'学習能力ゼロ',       desc:'スパークに5回以上やられた',    prog:{stat:'sparksDeaths',   target:5}    },
    { id:'no_rewind',        cat:'熟練', name:'痛みを知らぬ者',     desc:'3ステージ連続ノーダメージでクリアした' },

    // ===== 上級 (33) =====
    { id:'low_clear_20',     cat:'上級', name:'神速消滅',           desc:'侵食率20%以下でクリアした' },
    { id:'play_1000',        cat:'上級', name:'もう手遅れ',         desc:'累計1000回プレイした',         prog:{stat:'totalPlays',     target:1000} },
    { id:'clear_70',         cat:'上級', name:'七割制覇',           desc:'70種類のステージをクリアした' },
    { id:'clear_120',        cat:'上級', name:'ステージ漁り',       desc:'120種類のステージをクリアした' },
    { id:'all_free',         cat:'上級', name:'暇人の極み',         desc:'全フリーステージをクリアした' },
    { id:'all_80',           cat:'上級', name:'よく飽きないな',     desc:'全フリーステージで80%以上を達成した' },
    { id:'all_free_90',      cat:'上級', name:'完璧主義者',         desc:'全フリーステージで90%以上の侵食率を達成した' },
    { id:'erosion_95',       cat:'上級', name:'やりすぎ',           desc:'95%以上の侵食率でクリア' },
    { id:'erosion_97',       cat:'上級', name:'強迫観念',           desc:'97%以上の侵食率でクリア' },
    { id:'gameover_100',     cat:'上級', name:'百の屍を越えて',     desc:'累計100回ゲームオーバーになった', prog:{stat:'totalGameovers', target:100} },
    { id:'damage_300',       cat:'上級', name:'歴戦の証',           desc:'累計300回ダメージを受けた',    prog:{stat:'totalDamages',   target:300}  },
    { id:'damage_500',       cat:'上級', name:'それが個性です',     desc:'累計500回ダメージを受けた',    prog:{stat:'totalDamages',   target:500}  },
    { id:'five_trap',        cat:'上級', name:'一掃',               desc:'1回の囲みで5体以上を同時に撃破した' },
    { id:'big_40',           cat:'上級', name:'領域支配',           desc:'1回の囲みで領域を40%以上一気に増やした' },
    { id:'big_45',           cat:'上級', name:'欲の権化',           desc:'1回の囲みで領域を45%以上一気に増やした' },
    { id:'trap_500',         cat:'上級', name:'量産型ハンター',     desc:'累計500体の敵を囲んで撃破した', prog:{stat:'totalTraps',     target:500}  },
    { id:'trap_1000',        cat:'上級', name:'千の鎮魂',           desc:'累計1000体の敵を囲んで撃破した',prog:{stat:'totalTraps',     target:1000} },
    { id:'trap_octa',        cat:'上級', name:'八方塞がり返し',     desc:'八方向ビーム砲(OCTA_BEAM)を囲んで撃破した' },
    { id:'trap_sniper',      cat:'上級', name:'狙撃者を囲う',       desc:'狙撃ビーム(SNIPER_BEAM)を囲んで撃破した' },
    { id:'all_types',        cat:'上級', name:'差別なく倒す',       desc:'全9種類の敵を最低1体ずつトラップした' },
    { id:'mine_sweeper',     cat:'上級', name:'地雷マニア',         desc:'3ステージ以上で全地雷をトラップ除去した' },
    { id:'nodamage_5',       cat:'上級', name:'不動の境地',         desc:'5ステージ連続ノーダメージでクリアした' },
    { id:'long_trail',       cat:'上級', name:'引きずり屋',         desc:'累計で引いた軌跡が50万マス以上', prog:{stat:'totalTrailCells',target:500000}},
    { id:'long_trail_20000', cat:'上級', name:'地図を描く者',       desc:'累計で引いた軌跡が150万マス以上',prog:{stat:'totalTrailCells',target:1500000}},
    { id:'no_mine_explode',  cat:'上級', name:'完全処理',           desc:'全地雷をトラップで除去してクリア（爆破ゼロ）' },
    { id:'nodamage_10stages',cat:'上級', name:'傷なき征服者',       desc:'10種類のステージをノーダメージでクリアした' },
    { id:'enemy_wipe',       cat:'上級', name:'殲滅クリア',         desc:'全ての敵を撃破してステージをクリアした' },
    { id:'mine_3',           cat:'上級', name:'懲りない人',         desc:'地雷に3回以上連続で接触した' },
    { id:'long_trail_50000', cat:'上級', name:'軌跡の果て',         desc:'累計で引いた軌跡が500万マス以上',                prog:{stat:'totalTrailCells',target:5000000}},
    { id:'gameover_200',     cat:'上級', name:'不死鳥',             desc:'累計200回ゲームオーバーになった',                prog:{stat:'totalGameovers', target:200}  },
    { id:'damage_1000',      cat:'上級', name:'千の痛み',           desc:'累計1000回ダメージを受けた',                     prog:{stat:'totalDamages',   target:1000} },
    { id:'trap_2000',        cat:'上級', name:'殲滅者',             desc:'累計2000体の敵を囲んで撃破した',                 prog:{stat:'totalTraps',     target:2000} },
    { id:'sparks_death_10',  cat:'上級', name:'業火に誘われて',     desc:'スパークに10回以上やられた',                     prog:{stat:'sparksDeaths',   target:10}   },
    { id:'play_5000',        cat:'上級', name:'人生の一部',         desc:'累計5000回プレイした',                           prog:{stat:'totalPlays',     target:5000} },
    { id:'mine_sweeper_5',   cat:'上級', name:'爆発物マニア',       desc:'5ステージ以上で全地雷をトラップ除去した' },

    // ===== 伝説 (7, 全hidden) =====
    { id:'miracle',          cat:'伝説', name:'嘘みたい',           desc:'残MP20以下で95%以上の侵食率を達成した',          hidden:true },
    { id:'erosion_99',       cat:'伝説', name:'病的',               desc:'99%以上の侵食率でクリア',                        hidden:true },
    { id:'no_damage_all',    cat:'伝説', name:'人間をやめた',       desc:'全フリーステージをノーダメージでクリア',          hidden:true },
    { id:'seven_trap',       cat:'伝説', name:'七体まとめて',       desc:'1回の囲みで7体以上を同時に撃破した',             hidden:true },
    { id:'big_50',           cat:'伝説', name:'半分食べた',         desc:'1回の囲みで領域を50%以上一気に増やした',         hidden:true },
    { id:'nodamage_10',      cat:'伝説', name:'神域',               desc:'10ステージ連続ノーダメージでクリアした',         hidden:true },
    { id:'all_free_95',      cat:'伝説', name:'狂気の完璧主義',     desc:'全フリーステージで95%以上の侵食率を達成した',     hidden:true },
];

class AchievementSystem {
    constructor() {
        const saved = JSON.parse(localStorage.getItem('qix_achievements') || '{}');
        this.unlocked = new Set(saved.unlocked || []);

        const st = JSON.parse(localStorage.getItem('qix_stats') || '{}');
        this.stats = {
            totalTraps:            st.totalTraps            || 0,
            totalDamages:          st.totalDamages           || 0,
            totalPlays:            st.totalPlays             || 0,
            totalTrailCells:       st.totalTrailCells        || 0,
            stagesCleared:         new Set(st.stagesCleared       || []),
            bestErosion:           st.bestErosion            || {},
            typesTrapped:          new Set(st.typesTrapped        || []),
            consecMineHits:        st.consecMineHits          || 0,
            nodamageStreak:        st.nodamageStreak          || 0,
            nodamageStages:        new Set(st.nodamageStages       || []),
            minesAllClearedStages: new Set(st.minesAllClearedStages || []),
            totalGameovers:        st.totalGameovers          || 0,
            sparksDeaths:          st.sparksDeaths            || 0,
            creatorSaves:          st.creatorSaves            || 0,
            creatorClears:         st.creatorClears           || 0,
            galleryViewed:         st.galleryViewed           || false,
        };

        this.session = this._freshSession();
        this._queue  = [];
        this._showing = false;
    }

    _freshSession() {
        return {
            stageId:           null,
            isCreatorStage:    false,
            damaged:           false,
            minMp:             100,
            maxSingleTrap:     0,
            maxSingleErosion:  0,
            minesOnStage:      0,
            minesTrapped:      0,
            mineHitsOnPlayer:  0,
            stageHasBeamBoss:  false,
            stageHasMine:      false,
            stageHasSniper:    false,
            boostUsed:         false,
            sparksHits:        0,
        };
    }

    startStage(stageData) {
        this.session = this._freshSession();
        this.session.stageId          = stageData.id;
        this.session.isCreatorStage   = !!stageData._isCreator;
        this.session.stageHasBeamBoss = (stageData.enemies||[]).some(e => e.type === 'BEAM_BOSS');
        this.session.stageHasMine     = (stageData.enemies||[]).some(e => e.type === 'MINE');
        this.session.stageHasSniper   = (stageData.enemies||[]).some(e => e.type === 'SNIPER_BEAM');
        this.session.minesOnStage     = (stageData.enemies||[]).filter(e => e.type === 'MINE').length;
        this.stats.totalPlays++;
        this._save();
        this._check(['play_10','play_30','play_100','play_300','play_1000','play_5000']);
    }

    onGalleryView() {
        if (this.stats.galleryViewed) return;
        this.stats.galleryViewed = true;
        this._save();
        this.unlock('gallery_view');
    }

    onCreatorSave() {
        this.stats.creatorSaves++;
        this._save();
        this._check(['creator_made']);
    }

    onFriendlyFire() {
        this.unlock('friendly_fire');
    }

    onDamage() {
        this.session.damaged = true;
        this.stats.totalDamages++;
        this.stats.consecMineHits = 0;
        this.stats.nodamageStreak = 0;
        this._save();
        this._check(['damage_first','damage_10','damage_50','damage_100','damage_200','damage_300','damage_500','damage_1000']);
    }

    onSparksHit() {
        this.session.sparksHits++;
        this.stats.sparksDeaths++;
        this._save();
        this._check(['sparks_death','sparks_death_5','sparks_death_10']);
    }

    onBoostUsed() {
        this.session.boostUsed = true;
        this.unlock('first_boost');
    }

    onGameOver() {
        this.stats.totalGameovers++;
        this._save();
        this._check(['gameover_first','gameover_3','gameover_10','gameover_20','gameover_30','gameover_50','gameover_100','gameover_200']);
    }

    onMineHit() {
        this.session.mineHitsOnPlayer++;
        this.stats.consecMineHits++;
        this._save();
        if (this.stats.consecMineHits >= 3) this.unlock('mine_3');
    }

    onTrap(trappedEnemies, gainedCells, totalCount) {
        const count = trappedEnemies.length;
        const pct   = gainedCells / totalCount;

        if (count > 0) {
            this.stats.totalTraps += count;
            trappedEnemies.forEach(e => this.stats.typesTrapped.add(e.type));
            this.session.maxSingleTrap = Math.max(this.session.maxSingleTrap, count);
            this.session.minesTrapped += trappedEnemies.filter(e => e.type === 'MINE').length;
        }
        this.session.maxSingleErosion = Math.max(this.session.maxSingleErosion, pct);

        this._save();
        this._check([
            'first_trap','double_trap','triple_trap','four_trap','five_trap','seven_trap',
            'trap_10','trap_50','trap_100','trap_200','trap_500','trap_1000','trap_2000',
            'all_types',
            'trap_qix','trap_ghost','trap_shooter','trap_tracker','trap_snake',
            'trap_beamboss','trap_mine','trap_octa','trap_sniper',
            'big_20','big_25','big_30','big_35','big_40','big_45','big_50',
            'mine_sweep_3',
        ]);
    }

    onTrailCells(cells) {
        this.stats.totalTrailCells += cells;
        this._save();
        if (this.stats.totalTrailCells >= 100000)   this.unlock('trail_5000');
        if (this.stats.totalTrailCells >= 500000)   this.unlock('long_trail');
        if (this.stats.totalTrailCells >= 1500000)  this.unlock('long_trail_20000');
        if (this.stats.totalTrailCells >= 5000000)  this.unlock('long_trail_50000');
    }

    onMpUpdate(mp) {
        this.session.minMp = Math.min(this.session.minMp, mp);
    }

    onClear(finalPct, finalMp, allFreeStageData, allWiped = false) {
        this.session.clearMp = finalMp;
        const sid = this.session.stageId;
        const allFreeIds = allFreeStageData.map(s => s.id);

        if (sid != null) {
            this.stats.stagesCleared.add(sid);
            const prev = this.stats.bestErosion[sid] || 0;
            this.stats.bestErosion[sid] = Math.max(prev, finalPct);
        }

        if (!this.session.damaged) {
            this.stats.nodamageStreak++;
            if (sid != null) this.stats.nodamageStages.add(sid);
        } else {
            this.stats.nodamageStreak = 0;
        }

        const s = this.session;
        if (s.stageHasMine && s.mineHitsOnPlayer === 0 && s.minesTrapped === s.minesOnStage && sid != null) {
            this.stats.minesAllClearedStages.add(sid);
        }

        if (this.session.isCreatorStage) {
            this.stats.creatorClears++;
        }

        this._save();
        this._check([
            'first_clear',
            'clear_3','clear_5','clear_10','clear_20','clear_30','clear_50','clear_70','clear_120','all_free',
            'enemy_wipe','boost_nodamage',
            'all_80','all_free_90','all_free_95','no_damage_all',
            'low_clear_60','low_clear_40','low_clear_20',
            'erosion_70','erosion_80','erosion_85','erosion_90','erosion_92',
            'erosion_95','erosion_97','erosion_99',
            'big_20','big_25','big_30','big_35','big_40','big_45','big_50',
            'no_damage','tough','low_mp_clear',
            'no_rewind','nodamage_5','nodamage_10','nodamage_10stages',
            'beat_beamboss','beat_mine_stage','beat_sniper_stage',
            'no_mine_explode','mine_sweeper','mine_sweeper_5',
            'no_boost_clear','miracle',
        ], { finalPct, finalMp, allFreeIds, allWiped });
    }

    _check(ids, ctx = {}) {
        const s  = this.session;
        const st = this.stats;
        const { finalPct = 0, finalMp = 0, allFreeIds = [] } = ctx;

        for (const id of ids) {
            switch (id) {
                // ── クリア数 ──
                case 'first_clear':      if (st.stagesCleared.size >= 1)  this.unlock(id); break;
                case 'clear_3':          if (st.stagesCleared.size >= 3)  this.unlock(id); break;
                case 'clear_5':          if (st.stagesCleared.size >= 5)  this.unlock(id); break;
                case 'clear_10':         if (st.stagesCleared.size >= 10) this.unlock(id); break;
                case 'clear_20':         if (st.stagesCleared.size >= 20) this.unlock(id); break;
                case 'clear_30':         if (st.stagesCleared.size >= 30) this.unlock(id); break;
                case 'clear_50':         if (st.stagesCleared.size >= 50) this.unlock(id); break;
                case 'clear_70':         if (st.stagesCleared.size >= 70)  this.unlock(id); break;
                case 'clear_120':        if (st.stagesCleared.size >= 120) this.unlock(id); break;
                case 'all_free':         if (allFreeIds.length > 0 && allFreeIds.every(i => st.stagesCleared.has(i))) this.unlock(id); break;
                case 'creator_made':     if (st.creatorSaves >= 1)    this.unlock(id); break;
                case 'creator_clear':    if (st.creatorClears >= 1)   this.unlock(id); break;

                // ── 侵食率 ──
                case 'low_clear_60':     if (finalPct <= 0.60) this.unlock(id); break;
                case 'low_clear_40':     if (finalPct <= 0.40) this.unlock(id); break;
                case 'low_clear_20':     if (finalPct <= 0.20) this.unlock(id); break;
                case 'erosion_70':       if (finalPct >= 0.70) this.unlock(id); break;
                case 'erosion_80':       if (finalPct >= 0.80) this.unlock(id); break;
                case 'erosion_85':       if (finalPct >= 0.85) this.unlock(id); break;
                case 'erosion_90':       if (finalPct >= 0.90) this.unlock(id); break;
                case 'erosion_92':       if (finalPct >= 0.92) this.unlock(id); break;
                case 'erosion_95':       if (finalPct >= 0.95) this.unlock(id); break;
                case 'erosion_97':       if (finalPct >= 0.97) this.unlock(id); break;
                case 'erosion_99':       if (finalPct >= 0.99) this.unlock(id); break;

                // ── 全ステージ達成 ──
                case 'all_80':     if (allFreeIds.length > 0 && allFreeIds.every(i => (st.bestErosion[i]||0) >= 0.80)) this.unlock(id); break;
                case 'all_free_90':if (allFreeIds.length > 0 && allFreeIds.every(i => (st.bestErosion[i]||0) >= 0.90)) this.unlock(id); break;
                case 'all_free_95':if (allFreeIds.length > 0 && allFreeIds.every(i => (st.bestErosion[i]||0) >= 0.95)) this.unlock(id); break;

                // ── プレイ数 ──
                case 'play_10':          if (st.totalPlays >= 10)   this.unlock(id); break;
                case 'play_30':          if (st.totalPlays >= 30)   this.unlock(id); break;
                case 'play_100':         if (st.totalPlays >= 100)  this.unlock(id); break;
                case 'play_300':         if (st.totalPlays >= 300)  this.unlock(id); break;
                case 'play_1000':        if (st.totalPlays >= 1000) this.unlock(id); break;
                case 'play_5000':        if (st.totalPlays >= 5000) this.unlock(id); break;

                // ── ゲームオーバー ──
                case 'gameover_first':   if (st.totalGameovers >= 1)   this.unlock(id); break;
                case 'gameover_3':       if (st.totalGameovers >= 3)   this.unlock(id); break;
                case 'gameover_10':      if (st.totalGameovers >= 10)  this.unlock(id); break;
                case 'gameover_20':      if (st.totalGameovers >= 20)  this.unlock(id); break;
                case 'gameover_30':      if (st.totalGameovers >= 30)  this.unlock(id); break;
                case 'gameover_50':      if (st.totalGameovers >= 50)  this.unlock(id); break;
                case 'gameover_100':     if (st.totalGameovers >= 100) this.unlock(id); break;
                case 'gameover_200':     if (st.totalGameovers >= 200) this.unlock(id); break;

                // ── ダメージ ──
                case 'damage_first':     if (st.totalDamages >= 1)    this.unlock(id); break;
                case 'damage_10':        if (st.totalDamages >= 10)   this.unlock(id); break;
                case 'damage_50':        if (st.totalDamages >= 50)   this.unlock(id); break;
                case 'damage_100':       if (st.totalDamages >= 100)  this.unlock(id); break;
                case 'damage_200':       if (st.totalDamages >= 200)  this.unlock(id); break;
                case 'damage_300':       if (st.totalDamages >= 300)  this.unlock(id); break;
                case 'damage_500':       if (st.totalDamages >= 500)  this.unlock(id); break;
                case 'damage_1000':      if (st.totalDamages >= 1000) this.unlock(id); break;

                // ── 囲み数 (同時) ──
                case 'first_trap':       if (st.totalTraps >= 1)    this.unlock(id); break;
                case 'double_trap':      if (s.maxSingleTrap >= 2)  this.unlock(id); break;
                case 'triple_trap':      if (s.maxSingleTrap >= 3)  this.unlock(id); break;
                case 'four_trap':        if (s.maxSingleTrap >= 4)  this.unlock(id); break;
                case 'five_trap':        if (s.maxSingleTrap >= 5)  this.unlock(id); break;
                case 'seven_trap':       if (s.maxSingleTrap >= 7)  this.unlock(id); break;

                // ── 累計撃破数 ──
                case 'trap_10':          if (st.totalTraps >= 10)   this.unlock(id); break;
                case 'trap_50':          if (st.totalTraps >= 50)   this.unlock(id); break;
                case 'trap_100':         if (st.totalTraps >= 100)  this.unlock(id); break;
                case 'trap_200':         if (st.totalTraps >= 200)  this.unlock(id); break;
                case 'trap_500':         if (st.totalTraps >= 500)  this.unlock(id); break;
                case 'trap_1000':        if (st.totalTraps >= 1000) this.unlock(id); break;
                case 'trap_2000':        if (st.totalTraps >= 2000) this.unlock(id); break;

                // ── 敵種別撃破 ──
                case 'trap_qix':         if (st.typesTrapped.has('QIX'))         this.unlock(id); break;
                case 'trap_ghost':       if (st.typesTrapped.has('GHOST'))       this.unlock(id); break;
                case 'trap_shooter':     if (st.typesTrapped.has('SHOOTER'))     this.unlock(id); break;
                case 'trap_tracker':     if (st.typesTrapped.has('TRACKER'))     this.unlock(id); break;
                case 'trap_snake':       if (st.typesTrapped.has('SNAKE'))       this.unlock(id); break;
                case 'trap_beamboss':    if (st.typesTrapped.has('BEAM_BOSS'))   this.unlock(id); break;
                case 'trap_mine':        if (st.typesTrapped.has('MINE'))        this.unlock(id); break;
                case 'trap_octa':        if (st.typesTrapped.has('OCTA_BEAM'))   this.unlock(id); break;
                case 'trap_sniper':      if (st.typesTrapped.has('SNIPER_BEAM')) this.unlock(id); break;
                case 'all_types': {
                    const needed = ['QIX','SHOOTER','TRACKER','BEAM_BOSS','OCTA_BEAM','SNIPER_BEAM','SNAKE','MINE','GHOST'];
                    if (needed.every(t => st.typesTrapped.has(t))) this.unlock(id);
                    break;
                }

                // ── 大囲み (侵食%) ──
                case 'big_20':           if (s.maxSingleErosion >= 0.20) this.unlock(id); break;
                case 'big_25':           if (s.maxSingleErosion >= 0.25) this.unlock(id); break;
                case 'big_30':           if (s.maxSingleErosion >= 0.30) this.unlock(id); break;
                case 'big_35':           if (s.maxSingleErosion >= 0.35) this.unlock(id); break;
                case 'big_40':           if (s.maxSingleErosion >= 0.40) this.unlock(id); break;
                case 'big_45':           if (s.maxSingleErosion >= 0.45) this.unlock(id); break;
                case 'big_50':           if (s.maxSingleErosion >= 0.50) this.unlock(id); break;

                // ── スパーク ──
                case 'sparks_death':     if (st.sparksDeaths >= 1)  this.unlock(id); break;
                case 'sparks_death_5':   if (st.sparksDeaths >= 5)  this.unlock(id); break;
                case 'sparks_death_10':  if (st.sparksDeaths >= 10) this.unlock(id); break;

                // ── MP・ノーダメ ──
                case 'no_damage':        if (!s.damaged)              this.unlock(id); break;
                case 'tough':            if (s.minMp <= 30)           this.unlock(id); break;
                case 'low_mp_clear':     if (finalMp <= 10)           this.unlock(id); break;
                case 'miracle':          if (s.minMp <= 20 && finalPct >= 0.95) this.unlock(id); break;
                case 'no_boost_clear':   if (!s.boostUsed)            this.unlock(id); break;
                case 'boost_nodamage':   if (s.boostUsed && !s.damaged) this.unlock(id); break;
                case 'enemy_wipe':       if (ctx.allWiped)              this.unlock(id); break;

                // ── ノーダメ連続 ──
                case 'no_rewind':        if (st.nodamageStreak >= 3)  this.unlock(id); break;
                case 'nodamage_5':       if (st.nodamageStreak >= 5)  this.unlock(id); break;
                case 'nodamage_10':      if (st.nodamageStreak >= 10) this.unlock(id); break;
                case 'nodamage_10stages':if (st.nodamageStages.size >= 10) this.unlock(id); break;
                case 'no_damage_all': {
                    if (allFreeIds.length > 0 && allFreeIds.every(i => st.nodamageStages.has(i))) this.unlock(id);
                    break;
                }

                // ── ステージ特性クリア ──
                case 'beat_beamboss':    if (s.stageHasBeamBoss) this.unlock(id); break;
                case 'beat_mine_stage':  if (s.stageHasMine)     this.unlock(id); break;
                case 'beat_sniper_stage':if (s.stageHasSniper)   this.unlock(id); break;

                // ── 地雷系 ──
                case 'no_mine_explode':
                    if (s.stageHasMine && s.mineHitsOnPlayer === 0 && s.minesTrapped === s.minesOnStage) this.unlock(id); break;
                case 'mine_sweep_3':     if (s.minesTrapped >= 3)                       this.unlock(id); break;
                case 'mine_sweeper':     if (st.minesAllClearedStages.size >= 3)        this.unlock(id); break;
                case 'mine_sweeper_5':   if (st.minesAllClearedStages.size >= 5)        this.unlock(id); break;
            }
        }
    }

    unlock(id) {
        if (this.unlocked.has(id)) return;
        this.unlocked.add(id);
        this._save();
        this._notify(id);
    }

    _notify(id) {
        const a = ACHIEVEMENTS.find(a => a.id === id);
        if (!a) return;
        this._queue.push(a);
        if (!this._showing) this._showNext();
    }

    _showNext() {
        if (this._queue.length === 0) { this._showing = false; return; }
        this._showing = true;
        const a = this._queue.shift();
        const el = document.getElementById('achievement-toast');
        if (!el) { this._showing = false; return; }
        el.innerHTML = `
            <div>
                <div class="ach-toast-cat">${a.cat}</div>
                <div class="ach-toast-label">実績解除</div>
                <div class="ach-toast-name">${a.name}</div>
                <div class="ach-toast-desc">${a.desc}</div>
            </div>`;
        el.classList.add('show');
        setTimeout(() => {
            el.classList.remove('show');
            setTimeout(() => this._showNext(), 500);
        }, 3200);
    }

    _save() {
        localStorage.setItem('qix_achievements', JSON.stringify({
            unlocked: [...this.unlocked],
        }));
        localStorage.setItem('qix_stats', JSON.stringify({
            totalTraps:            this.stats.totalTraps,
            totalDamages:          this.stats.totalDamages,
            totalPlays:            this.stats.totalPlays,
            totalTrailCells:       this.stats.totalTrailCells,
            stagesCleared:         [...this.stats.stagesCleared],
            bestErosion:           this.stats.bestErosion,
            typesTrapped:          [...this.stats.typesTrapped],
            consecMineHits:        this.stats.consecMineHits,
            nodamageStreak:        this.stats.nodamageStreak,
            nodamageStages:        [...this.stats.nodamageStages],
            minesAllClearedStages: [...this.stats.minesAllClearedStages],
            totalGameovers:        this.stats.totalGameovers,
            sparksDeaths:          this.stats.sparksDeaths,
            creatorSaves:          this.stats.creatorSaves,
            creatorClears:         this.stats.creatorClears,
            galleryViewed:         this.stats.galleryViewed,
        }));
    }

    isUnlocked(id) { return this.unlocked.has(id); }

    getProgress() {
        return { unlocked: this.unlocked.size, total: ACHIEVEMENTS.length };
    }
}
