WikiGacha Menu

Easy tool for WikiGacha

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         WikiGacha Menu
// @name:ja      Wikiガチャ メニュー
// @version      1.0
// @description  Easy tool for WikiGacha
// @description:ja WikiGachaを簡単にするツール
// @match        *://*.wikigacha.com/*
// @icon         https://wikigacha.com/wikipedia_pack_1.png
// @license      MIT
// @namespace https://greasyfork.org/users/1577658
// ==/UserScript==
(function () {
    'use strict';

    const originalFetch = window.fetch;
    window.fetch = async function(...args) {
        const reqUrl = typeof args[0] === 'string' ? args[0] : (args[0] && args[0].url ? args[0].url : '');

        if (reqUrl.includes('/api/card?id=')) {
            const response = await originalFetch.apply(this, args);
            if (response.status === 404 || response.status === 500 || response.status === 403) {
                let id = 0;
                try {
                    const urlObj = new URL(reqUrl, window.location.origin);
                    id = Number(urlObj.searchParams.get('id'));
                } catch(e){}
                let foundCard = null;
                try {
                    const db = await new Promise((resolve, reject) => {
                        const req = indexedDB.open('wiki-gacha-db');
                        req.onsuccess = e => resolve(e.target.result);
                        req.onerror = e => reject(e);
                    });
                    const stores = ['cards_jp', 'cards_en'].filter(s => db.objectStoreNames.contains(s));
                    for (const storeName of stores) {
                        const tx = db.transaction([storeName], 'readonly');
                        const store = tx.objectStore(storeName);
                        const result = await new Promise(res => {
                            const getReq = store.get(id);
                            getReq.onsuccess = ev => res(ev.target.result);
                            getReq.onerror = () => res(null);
                        });
                        if (result) { foundCard = result; break; }
                    }
                    db.close();
                } catch(e) {}
                if (foundCard) {
                    const responseData = { ...foundCard, card: foundCard };
                    return new Response(JSON.stringify(responseData), {
                        status: 200,
                        statusText: 'OK',
                        headers: { 'Content-Type': 'application/json' }
                    });
                } else {
                    const dummyCard = {
                        id: id || 999999999,
                        title: "Protected Card",
                        extract: "Deleted from the server, but protected.",
                        abstract: "Deleted from the server, but protected.",
                        rarity_rank: "C"
                    };
                    return new Response(JSON.stringify({ ...dummyCard, card: dummyCard }), {
                        status: 200,
                        statusText: 'OK',
                        headers: { 'Content-Type': 'application/json' }
                    });
                }
            }
            return response;
        }
        return originalFetch.apply(this, args);
    };
    const originalDelete = IDBObjectStore.prototype.delete;
    window.__wgcm_allow_delete = false;
    IDBObjectStore.prototype.delete = function(query) {
        if ((this.name === 'cards_jp' || this.name === 'cards_en') && !window.__wgcm_allow_delete) {
            return originalDelete.call(this, -1);
        }
        return originalDelete.apply(this, arguments);
    };
    const originalAlert = window.alert;
    window.alert = function(msg) {
        if (typeof msg === 'string' && msg.includes('図鑑からこのカードを除外しました')) return;
        return originalAlert.apply(this, arguments);
    };
    const observer = new MutationObserver(mutations => {
        mutations.forEach(mutation => {
            mutation.addedNodes.forEach(node => {
                if (node.nodeType === Node.ELEMENT_NODE && node.textContent && node.textContent.includes('図鑑からこのカードを除外しました')) {
                    node.style.display = 'none';
                }
            });
        });
    });
    if (document.documentElement) {
        observer.observe(document.documentElement, { childList: true, subtree: true });
    }
    async function saveCardsToDB(cards, lang = 'JP') {
        return new Promise((resolve, reject) => {
            const storeName = lang === 'EN' ? 'cards_en' : 'cards_jp';
            const request = indexedDB.open('wiki-gacha-db');
            request.onerror = e => reject('DB Error: ' + e.target.error);
            request.onsuccess = e => {
                const db = e.target.result;
                if (!db.objectStoreNames.contains(storeName)) {
                    db.close();
                    return reject('Save data not found. Please pull the gacha manually at least once.');
                }
                const tx = db.transaction([storeName], 'readwrite');
                const store = tx.objectStore(storeName);
                let newCount = 0, now = Date.now();
                cards.forEach(cardData => {
                    const getReq = store.get(cardData.id);
                    getReq.onsuccess = ev => {
                        const existing = ev.target.result;
                        let finalCard = { ...cardData };
                        if (existing) {
                            finalCard.count = (existing.count || 1) + 1;
                            finalCard.first_obtained_at = existing.first_obtained_at || now;
                            finalCard.is_favorite = !!existing.is_favorite;
                        } else {
                            finalCard.count = 1;
                            finalCard.first_obtained_at = now + newCount++;
                            finalCard.is_favorite = !!cardData.is_favorite;
                        }
                        store.put(finalCard);
                    };
                });
                tx.oncomplete = () => { db.close(); resolve(); };
                tx.onerror = ev => { db.close(); reject('Save failed: ' + ev.target.error); };
            };
        });
    }
    const style = document.createElement('style');
    style.textContent = `
    #wgcm-wrap * { box-sizing: border-box; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; }
    #wgcm-wrap { position:fixed; top:50px; right:10px; width:280px; background:#111827; color:#f3f4f6;
        border:1px solid #1f2937; border-radius:10px; z-index:999999;
        box-shadow:0 8px 32px rgba(0,0,0,.6); overflow:hidden; }
    #wgcm-header { display:flex; align-items:center; justify-content:space-between;
        padding:8px 12px; background:#1f2937; border-bottom:1px solid #374151; cursor:move; user-select:none; }
    #wgcm-header span { font-size:12px; font-weight:700; letter-spacing:.5px; color:#9ca3af; }
    #wgcm-collapse { background:none; border:none; color:#6b7280; cursor:pointer; font-size:14px; padding:0; line-height:1; }
    #wgcm-collapse:hover { color:#f3f4f6; }

    #wgcm-tabs { display:flex; background:#1a2436; border-bottom:1px solid #1f2937; padding:0 4px; gap:2px; }
    .wg-tab-btn { flex:1; background:none; border:none; color:#6b7280; font-size:10px; font-weight:700;
        padding:8px 0; cursor:pointer; transition:all .15s; border-bottom:2px solid transparent; }
    .wg-tab-btn:hover { color:#d1d5db; background:rgba(255,255,255,.05); }
    .wg-tab-btn.active { color:#3b82f6; border-bottom-color:#3b82f6; background:rgba(59,130,246,.1); }
    #wgcm-body { padding:10px; display:flex; flex-direction:column; gap:10px; max-height:80vh; overflow-y:auto; }
    #wgcm-body::-webkit-scrollbar { width:4px; }
    #wgcm-body::-webkit-scrollbar-thumb { background:#374151; border-radius:2px; }
    .wg-section { background:#1a2436; border:1px solid #1f2937; border-radius:8px; padding:8px 10px; display:none; }
    .wg-section.active { display:block; }
    .wg-section-title { font-size:10px; font-weight:700; color:#6b7280; text-transform:uppercase;
        letter-spacing:.8px; margin-bottom:8px; }
    .wg-row { display:flex; align-items:center; gap:6px; margin-bottom:6px; }
    .wg-row:last-child { margin-bottom:0; }
    .wg-label { font-size:11px; color:#9ca3af; white-space:nowrap; }
    .wg-select, .wg-input { flex:1; background:#0f172a; border:1px solid #374151; border-radius:5px;
        color:#f3f4f6; font-size:12px; padding:5px 7px; outline:none; width:100%; }
    .wg-select:focus, .wg-input:focus { border-color:#3b82f6; }
    .wg-input::placeholder { color:#4b5563; }
    .wg-stats { background:#0f172a; border-radius:5px; padding:6px 8px; font-size:11px;
        font-family:ui-monospace, monospace; color:#d1d5db; display:grid; grid-template-columns:1fr 1fr; gap:1px 12px; }
    .wg-stats-title { grid-column:1/-1; font-weight:700; color:#6b7280; font-size:10px;
        text-transform:uppercase; letter-spacing:.5px; margin-bottom:3px; }
    .wg-stat { display:flex; justify-content:space-between; }
    .wg-stat-key { color:#6b7280; }
    .wg-stat-val { font-weight:700; color:#f3f4f6; font-variant-numeric:tabular-nums; }
    .wg-log { font-size:10px; color:#6b7280; margin-top:4px; min-height:14px;
        white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
    .wg-btn { border:none; border-radius:5px; cursor:pointer; font-size:11px; font-weight:700;
        padding:6px 10px; color:#fff; transition:opacity .15s; white-space:nowrap; flex-shrink:0; }
    .wg-btn:hover { opacity:.85; }
    .wg-btn:active { opacity:.7; }
    .wg-btn-blue { background:#2563eb; }
    .wg-btn-red { background:#dc2626; }
    .wg-btn-gray { background:#374151; }
    .wg-btn-green { background:#059669; }
    .wg-btn-purple { background:#7c3aed; }
    .wg-btn-full { width:100%; text-align:center; }
    #wgcm-toggle { position:fixed; top:10px; right:10px; z-index:1000000;
        background:#ef4444; color:#fff; border:none; border-radius:20px;
        padding:6px 12px; font-size:12px; font-weight:700; cursor:pointer;
        box-shadow:0 2px 8px rgba(0,0,0,.4); }
    #wgcm-toggle:hover { opacity:.9; }
    `;
    document.head.appendChild(style);
    let autoGachaRunning = false;
    let autoCompRunning = false;
    let autoCompStats = { added: 0, skipped: 0, failed: 0, currentId: 1 };
    let currentPackState = null;
    let stats = { total: 0, LR: 0, UR: 0, SSR: 0, SR: 0, R: 0, UC: 0, C: 0 };
    const wrap = document.createElement('div');
    wrap.id = 'wgcm-wrap';
    const header = document.createElement('div');
    header.id = 'wgcm-header';
    header.innerHTML = `<span>WIKIGACHA MENU</span>`;
    const collapseBtn = document.createElement('button');
    collapseBtn.id = 'wgcm-collapse';
    collapseBtn.innerText = '▼';
    header.appendChild(collapseBtn);
    wrap.appendChild(header);

    const tabsContainer = document.createElement('div');
    tabsContainer.id = 'wgcm-tabs';
    const tabs = [
        { id: 'tab-gacha', label: 'Gacha' },
        { id: 'tab-create', label: 'Create' },
        { id: 'tab-get', label: 'Get' },
        { id: 'tab-tamper', label: 'Tamper' },
        { id: 'tab-trophy', label: 'Trophy' },
        { id: 'tab-comp', label: 'Comp' }
    ];
    const tabButtons = [];
    tabs.forEach((tab, index) => {
        const btn = document.createElement('button');
        btn.className = 'wg-tab-btn' + (index === 0 ? ' active' : '');
        btn.innerText = tab.label;
        btn.onclick = () => switchTab(index);
        tabsContainer.appendChild(btn);
        tabButtons.push(btn);
    });
    wrap.appendChild(tabsContainer);
    const body = document.createElement('div');
    body.id = 'wgcm-body';
    wrap.appendChild(body);
    const sections = [];
    function switchTab(index) {
        sections.forEach((sec, i) => {
            sec.classList.toggle('active', i === index);
            tabButtons[i].classList.toggle('active', i === index);
        });
    }
    let dragOX = 0, dragOY = 0, dragging = false;
    header.addEventListener('mousedown', e => {
        dragging = true;
        dragOX = e.clientX - wrap.getBoundingClientRect().left;
        dragOY = e.clientY - wrap.getBoundingClientRect().top;
    });
    document.addEventListener('mousemove', e => {
        if (!dragging) return;
        wrap.style.right = 'auto';
        wrap.style.left = (e.clientX - dragOX) + 'px';
        wrap.style.top = (e.clientY - dragOY) + 'px';
    });
    document.addEventListener('mouseup', () => dragging = false);
    let collapsed = false;
    collapseBtn.addEventListener('click', () => {
        collapsed = !collapsed;
        body.style.display = collapsed ? 'none' : 'flex';
        tabsContainer.style.display = collapsed ? 'none' : 'flex';
        collapseBtn.innerText = collapsed ? '▶' : '▼';
    });
    const sec1 = document.createElement('div');
    sec1.className = 'wg-section active';
    sections.push(sec1);
    sec1.innerHTML = `<div class="wg-section-title">Auto Gacha</div>`;
    const settingRow = document.createElement('div');
    settingRow.className = 'wg-row';
    settingRow.innerHTML = `<span class="wg-label">Settings</span>`;
    const srSelect = document.createElement('select');
    srSelect.className = 'wg-select';
    srSelect.innerHTML = `<option value="0">0 (Normal)</option><option value="1">1 (SR+ Guaranteed)</option>`;
    settingRow.appendChild(srSelect);
    sec1.appendChild(settingRow);
    const statsDiv = document.createElement('div');
    statsDiv.className = 'wg-stats';
    function updateStatsDisplay() {
        statsDiv.innerHTML = `
            <div class="wg-stats-title">Stats <span style="color:#f3f4f6;font-weight:700;float:right">${stats.total} pulls</span></div>
            ${['LR','UR','SSR','SR','R','UC','C'].map(r =>
                `<div class="wg-stat"><span class="wg-stat-key">${r}</span><span class="wg-stat-val">${stats[r]}</span></div>`
            ).join('')}
        `;
    }
    updateStatsDisplay();
    sec1.appendChild(statsDiv);
    const btnRow = document.createElement('div');
    btnRow.className = 'wg-row';
    btnRow.style.marginTop = '6px';
    const autoGachaBtn = document.createElement('button');
    autoGachaBtn.className = 'wg-btn wg-btn-blue';
    autoGachaBtn.style.flex = '1';
    autoGachaBtn.innerText = '▶ Start';
    const resetBtn = document.createElement('button');
    resetBtn.className = 'wg-btn wg-btn-gray';
    resetBtn.innerText = '↺';
    resetBtn.title = 'Reset';
    resetBtn.style.padding = '6px 10px';
    resetBtn.onclick = () => {
        stats = { total: 0, LR: 0, UR: 0, SSR: 0, SR: 0, R: 0, UC: 0, C: 0 };
        updateStatsDisplay();
    };
    btnRow.appendChild(autoGachaBtn);
    btnRow.appendChild(resetBtn);
    sec1.appendChild(btnRow);
    const logDiv = document.createElement('div');
    logDiv.className = 'wg-log';
    logDiv.innerText = 'Waiting...';
    sec1.appendChild(logDiv);
    body.appendChild(sec1);
    async function initPackState() {
        const res = await fetch('/api/pack', {
            method: 'POST', headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({ action: 'init' })
        });
        return (await res.json()).packState;
    }
    async function doApiGachaLoop() {
        if (!autoGachaRunning) return;
        try {
            if (!currentPackState) {
                logDiv.innerText = 'Initializing...';
                currentPackState = await initPackState();
            }
            currentPackState.balance = 10;
            const res = await fetch('/api/gacha', {
                method: 'POST', headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify({ guaranteedSrPlus: parseInt(srSelect.value), lang: 'JP', packState: currentPackState })
            });
            if (!res.ok) throw new Error('HTTP ' + res.status);
            const data = await res.json();
            if (data.packState) currentPackState = data.packState;
            if (data.cards?.length) {
                const rarities = [];
                data.cards.forEach(card => {
                    const r = card.rarity_rank || 'C';
                    stats.total++; if (stats[r] !== undefined) stats[r]++;
                    rarities.push(`[${r}]`);
                });
                updateStatsDisplay();
                logDiv.innerText = rarities.join(' ');
                await saveCardsToDB(data.cards, 'JP');
            }
            setTimeout(doApiGachaLoop, 300);
        } catch (e) {
            logDiv.innerText = 'Error: ' + e.message;
            currentPackState = null;
            setTimeout(doApiGachaLoop, 2000);
        }
    }
    autoGachaBtn.onclick = () => {
        autoGachaRunning = !autoGachaRunning;
        if (autoGachaRunning) {
            autoGachaBtn.className = 'wg-btn wg-btn-red';
            autoGachaBtn.innerText = '■ Stop';
            doApiGachaLoop();
        } else {
            autoGachaBtn.className = 'wg-btn wg-btn-blue';
            autoGachaBtn.innerText = '▶ Start';
            logDiv.innerText = 'Stopped.';
        }
    };
    const sec2 = document.createElement('div');
    sec2.className = 'wg-section';
    sections.push(sec2);
    sec2.innerHTML = `<div class="wg-section-title">Create Original Card</div>`;
    function mkInput(placeholder, type = 'text') {
        const el = document.createElement('input');
        el.type = type; el.placeholder = placeholder; el.className = 'wg-input';
        return el;
    }
    const extraStyle = document.createElement('style');
    extraStyle.textContent = `
    .wg-slider-row { display:flex; align-items:center; gap:6px; margin-bottom:6px; }
    .wg-slider-label { font-size:10px; color:#6b7280; width:28px; flex-shrink:0; }
    .wg-slider { flex:1; -webkit-appearance:none; appearance:none; height:4px;
        border-radius:2px; background:#374151; outline:none; cursor:pointer; }
    .wg-slider::-webkit-slider-thumb { -webkit-appearance:none; appearance:none;
        width:14px; height:14px; border-radius:50%; background:#3b82f6; cursor:pointer; }
    .wg-slider-val { font-size:11px; font-weight:700; color:#f3f4f6;
        width:42px; text-align:right; font-variant-numeric:tabular-nums; flex-shrink:0; }
    .wg-slider-val input { width:42px; background:#0f172a; border:1px solid #374151;
        border-radius:4px; color:#f3f4f6; font-size:11px; font-weight:700;
        text-align:right; padding:2px 4px; outline:none; font-variant-numeric:tabular-nums; }
    .wg-slider-val input:focus { border-color:#3b82f6; }
    .wg-slider-val input::-webkit-inner-spin-button,
    .wg-slider-val input::-webkit-outer-spin-button { -webkit-appearance:none; margin:0; }
    .wg-slider-val input[type=number] { -moz-appearance:textfield; }
    .wg-rarity-grid { display:grid; grid-template-columns:repeat(7,1fr); gap:3px; margin-bottom:6px; }
    .wg-rarity-btn { border:1px solid #374151; border-radius:4px; background:#0f172a;
        color:#6b7280; font-size:10px; font-weight:700; padding:4px 0; cursor:pointer;
        text-align:center; transition:all .1s; }
    .wg-rarity-btn:hover { border-color:#6b7280; color:#d1d5db; }
    .wg-rarity-btn.active { color:#fff; border-color:transparent; }
    .wg-rarity-btn[data-r="LR"].active  { background:#dc2626; }
    .wg-rarity-btn[data-r="UR"].active  { background:#d97706; }
    .wg-rarity-btn[data-r="SSR"].active { background:#7c3aed; }
    .wg-rarity-btn[data-r="SR"].active  { background:#2563eb; }
    .wg-rarity-btn[data-r="R"].active   { background:#059669; }
    .wg-rarity-btn[data-r="UC"].active  { background:#0891b2; }
    .wg-rarity-btn[data-r="C"].active   { background:#4b5563; }
    `;
    document.head.appendChild(extraStyle);
    const customTitle = mkInput('Card Name');
        customTitle.style.marginBottom = '6px';
    sec2.appendChild(customTitle);
    const customImage = document.createElement('input');
    customImage.type = 'file';
    customImage.accept = 'image/*';
    customImage.className = 'wg-input';
    customImage.style.marginBottom = '6px';
    customImage.style.padding = '4px';
    sec2.appendChild(customImage);
    const customDescription = document.createElement('textarea');
    customDescription.placeholder = 'Description (e.g. Wikipedia summary)';
    customDescription.className = 'wg-input';
    customDescription.style.marginBottom = '6px';
    customDescription.style.resize = 'vertical';
    customDescription.style.height = '60px';
    sec2.appendChild(customDescription);
    const customFlavorText = document.createElement('textarea');
    customFlavorText.placeholder = 'Flavor Text';
    customFlavorText.className = 'wg-input';
    customFlavorText.style.marginBottom = '6px';
    customFlavorText.style.resize = 'vertical';
    customFlavorText.style.height = '60px';
    sec2.appendChild(customFlavorText);
    function makeStatSlider(label, color) {
        const row = document.createElement('div');
        row.className = 'wg-slider-row';
        const lbl = document.createElement('span');
        lbl.className = 'wg-slider-label';
        lbl.innerText = label;
        const slider = document.createElement('input');
        slider.type = 'range'; slider.className = 'wg-slider';
        slider.min = 0; slider.max = 99999; slider.value = 0;
        slider.style.setProperty('--c', color);
        slider.style.cssText += `accent-color:${color}`;
        const valWrap = document.createElement('div');
        valWrap.className = 'wg-slider-val';
        const numInput = document.createElement('input');
        numInput.type = 'number'; numInput.value = 0; numInput.min = 0;
        valWrap.appendChild(numInput);
        slider.addEventListener('input', () => { numInput.value = slider.value; });
        numInput.addEventListener('input', () => {
            const v = Math.max(0, parseInt(numInput.value) || 0);
            slider.value = Math.min(v, 99999);
            numInput.value = v;
        });
        row.appendChild(lbl); row.appendChild(slider); row.appendChild(valWrap);
        return { row, getValue: () => parseInt(numInput.value) || 0 };
    }
    const atkSlider = makeStatSlider('ATK', '#ef4444');
    const defSlider = makeStatSlider('DEF', '#3b82f6');
    sec2.appendChild(atkSlider.row);
    sec2.appendChild(defSlider.row);
    const rarityRow = document.createElement('div');
    rarityRow.className = 'wg-row';
    const rarityLabel = document.createElement('span');
    rarityLabel.className = 'wg-label';
    rarityLabel.innerText = 'Rarity';
    rarityLabel.style.width = '55px';
    const rarityInput = mkInput('SR');
    rarityInput.value = 'SR';
    rarityRow.appendChild(rarityLabel);
    rarityRow.appendChild(rarityInput);
    sec2.appendChild(rarityRow);
    const rarityGrid = document.createElement('div');
    rarityGrid.className = 'wg-rarity-grid';
    ['LR','UR','SSR','SR','R','UC','C'].forEach(r => {
        const btn = document.createElement('button');
        btn.className = 'wg-rarity-btn' + (r === rarityInput.value ? ' active' : '');
        btn.dataset.r = r;
        btn.innerText = r;
        btn.onclick = () => {
            rarityGrid.querySelectorAll('.wg-rarity-btn').forEach(b => b.classList.remove('active'));
            btn.classList.add('active');
            rarityInput.value = r;
        };
        rarityGrid.appendChild(btn);
    });
    rarityInput.addEventListener('input', () => {
        rarityGrid.querySelectorAll('.wg-rarity-btn').forEach(b => {
            b.classList.toggle('active', b.dataset.r === rarityInput.value);
        });
    });
    sec2.appendChild(rarityGrid);
    const createBtn = document.createElement('button');
    createBtn.className = 'wg-btn wg-btn-green wg-btn-full';
    createBtn.innerText = '+ Register to Collection';
    createBtn.onclick = async () => {
        if (!customTitle.value.trim()) return alert('Please enter a card name');
        let imgData = '';
        if (customImage.files && customImage.files[0]) {
            const file = customImage.files[0];
            imgData = await new Promise(resolve => {
                const reader = new FileReader();
                reader.onload = e => resolve(e.target.result);
                reader.readAsDataURL(file);
            });
        }
        try {
            await saveCardsToDB([{
                id: Math.floor(Math.random() * 1e8) + 1e8,
                title: customTitle.value.trim(),
                extract: customDescription.value.trim() || '',
                abstract: customDescription.value.trim() || '',
                flavor_text: customFlavorText.value.trim() || '',
                lang: 'JP',
                rarity_rank: rarityInput.value,
                true_attack: atkSlider.getValue(),
                true_defense: defSlider.getValue(),
                image_url: imgData
            }], 'JP');
            alert('Saved to collection!\nPlease reload the page to confirm.');

            customTitle.value = '';
            customImage.value = '';
            customDescription.value = '';
            customFlavorText.value = '';
            atkSlider.row.querySelector('.wg-slider').value = 0;
            atkSlider.row.querySelector('input[type="number"]').value = 0;
            defSlider.row.querySelector('.wg-slider').value = 0;
            defSlider.row.querySelector('input[type="number"]').value = 0;
            rarityInput.value = 'SR';
            rarityGrid.querySelectorAll('.wg-rarity-btn').forEach(b => {
                b.classList.toggle('active', b.dataset.r === 'SR');
            });
        } catch (e) { }
    };
    sec2.appendChild(createBtn);
    body.appendChild(sec2);
    const sec3 = document.createElement('div');
    sec3.className = 'wg-section';
    sections.push(sec3);
    sec3.innerHTML = `<div class="wg-section-title">Get Card by ID</div>`;
    const idRow = document.createElement('div');
    idRow.className = 'wg-row';
    const idInput = mkInput('Card ID');
    const getBtn = document.createElement('button');
    getBtn.className = 'wg-btn wg-btn-purple';
    getBtn.innerText = 'Get';
    idRow.appendChild(idInput); idRow.appendChild(getBtn);
    sec3.appendChild(idRow);
    const idLog = document.createElement('div');
    idLog.className = 'wg-log';
    sec3.appendChild(idLog);
    body.appendChild(sec3);
    getBtn.onclick = async () => {
        const id = idInput.value.replace(/\D/g, '');
        if (!id) { idLog.innerText = '⚠ Please enter an ID'; return; }
        getBtn.innerText = '...'; idLog.innerText = 'Fetching...';
        try {
            const res = await fetch(`/api/card?id=${id}&lang=JP`);
            if (!res.ok) throw new Error('Card not found.');
            const data = await res.json();
            const card = data.card || data;
            if (card?.id) {
                await saveCardsToDB([card], card.lang || 'JP');
                idLog.innerText = `✓ "${card.title}" registered`;
            } else { idLog.innerText = '⚠ Data was empty'; }
        } catch (e) { idLog.innerText = '✗ ' + e.message; }
        finally { getBtn.innerText = 'Get'; }
    };
    const sec4 = document.createElement('div');
    sec4.className = 'wg-section';
    sections.push(sec4);
    sec4.innerHTML = `<div class="wg-section-title">Status Tamper</div>`;
    const tamperIdRow = document.createElement('div');
    tamperIdRow.className = 'wg-row';
    const tamperIdInput = mkInput('Target Card ID');
    const loadBtn = document.createElement('button');
    loadBtn.className = 'wg-btn wg-btn-blue';
    loadBtn.innerText = 'Load';
    tamperIdRow.appendChild(tamperIdInput); tamperIdRow.appendChild(loadBtn);
    sec4.appendChild(tamperIdRow);
    const tamperTitle = mkInput('Card Name');
    tamperTitle.style.marginBottom = '6px';
    sec4.appendChild(tamperTitle);
    const tamperImage = document.createElement('input');
    tamperImage.type = 'file';
    tamperImage.accept = 'image/*';
    tamperImage.className = 'wg-input';
    tamperImage.style.marginBottom = '6px';
    tamperImage.style.padding = '4px';
    sec4.appendChild(tamperImage);
    const tamperDescription = document.createElement('textarea');
    tamperDescription.placeholder = 'Description (e.g. Wikipedia summary)';
    tamperDescription.className = 'wg-input';
    tamperDescription.style.marginBottom = '6px';
    tamperDescription.style.resize = 'vertical';
    tamperDescription.style.height = '60px';
    sec4.appendChild(tamperDescription);
    const tamperFlavorText = document.createElement('textarea');
    tamperFlavorText.placeholder = 'Flavor Text';
    tamperFlavorText.className = 'wg-input';
    tamperFlavorText.style.marginBottom = '6px';
    tamperFlavorText.style.resize = 'vertical';
    tamperFlavorText.style.height = '60px';
    sec4.appendChild(tamperFlavorText);
    const tamperAtkSlider = makeStatSlider('ATK', '#ef4444');
    const tamperDefSlider = makeStatSlider('DEF', '#3b82f6');
    sec4.appendChild(tamperAtkSlider.row);
    sec4.appendChild(tamperDefSlider.row);
    const tamperCountRow = document.createElement('div');
    tamperCountRow.className = 'wg-row';
    const tamperCountLabel = document.createElement('span');
    tamperCountLabel.className = 'wg-label';
    tamperCountLabel.innerText = 'Count';
    tamperCountLabel.style.width = '28px';
    const tamperCountInput = document.createElement('input');
    tamperCountInput.type = 'number'; tamperCountInput.className = 'wg-input'; tamperCountInput.value = 1; tamperCountInput.min = 1;
    tamperCountRow.appendChild(tamperCountLabel); tamperCountRow.appendChild(tamperCountInput);
    sec4.appendChild(tamperCountRow);
    const tamperRarityRow = document.createElement('div');
    tamperRarityRow.className = 'wg-row';
    const tamperRarityLabel = document.createElement('span');
    tamperRarityLabel.className = 'wg-label';
    tamperRarityLabel.innerText = 'Rarity';
    tamperRarityLabel.style.width = '55px';
    const tamperRarityInput = mkInput('SR');
    tamperRarityInput.value = 'SR';
    tamperRarityRow.appendChild(tamperRarityLabel);
    tamperRarityRow.appendChild(tamperRarityInput);
    sec4.appendChild(tamperRarityRow);
    const tamperRarityGrid = document.createElement('div');
    tamperRarityGrid.className = 'wg-rarity-grid';
    ['LR','UR','SSR','SR','R','UC','C'].forEach(r => {
        const btn = document.createElement('button');
        btn.className = 'wg-rarity-btn' + (r === tamperRarityInput.value ? ' active' : '');
        btn.dataset.r = r;
        btn.innerText = r;
        btn.onclick = () => {
            tamperRarityGrid.querySelectorAll('.wg-rarity-btn').forEach(b => b.classList.remove('active'));
            btn.classList.add('active');
            tamperRarityInput.value = r;
        };
        tamperRarityGrid.appendChild(btn);
    });
    tamperRarityInput.addEventListener('input', () => {
        tamperRarityGrid.querySelectorAll('.wg-rarity-btn').forEach(b => {
            b.classList.toggle('active', b.dataset.r === tamperRarityInput.value);
        });
    });
    sec4.appendChild(tamperRarityGrid);
    const tamperBtn = document.createElement('button');
    tamperBtn.className = 'wg-btn wg-btn-red wg-btn-full';
    tamperBtn.innerText = '⚠ Execute Tamper';
    const tamperLog = document.createElement('div');
    tamperLog.className = 'wg-log';
    sec4.appendChild(tamperBtn);
    sec4.appendChild(tamperLog);
    body.appendChild(sec4);
    let loadedCardData = null;
    loadBtn.onclick = async () => {
        const id = Number(tamperIdInput.value.replace(/\D/g, ''));
        if (!id) { tamperLog.innerText = '⚠ Please enter an ID'; return; }
        tamperLog.innerText = 'Loading...';
        try {
            const db = await new Promise((resolve, reject) => {
                const req = indexedDB.open('wiki-gacha-db');
                req.onerror = e => reject('DB Error: ' + e.target.error);
                req.onsuccess = e => resolve(e.target.result);
            });
            const stores = ['cards_jp', 'cards_en'].filter(s => db.objectStoreNames.contains(s));
            let found = null;
            let foundStore = null;
            for (const storeName of stores) {
                const tx = db.transaction([storeName], 'readonly');
                const store = tx.objectStore(storeName);
                const result = await new Promise(res => {
                    const getReq = store.get(id);
                    getReq.onsuccess = ev => res(ev.target.result);
                    getReq.onerror = () => res(null);
                });
                if (result) { found = result; foundStore = storeName; break; }
            }
            db.close();
            if (found) {
                loadedCardData = { card: found, store: foundStore };
                tamperTitle.value = found.title || '';
                tamperDescription.value = found.extract || found.abstract || '';
                tamperFlavorText.value = found.flavor_text || '';
                tamperImage.value = '';
                tamperAtkSlider.row.querySelector('.wg-slider').value = found.true_attack || 0;
                tamperAtkSlider.row.querySelector('input[type="number"]').value = found.true_attack || 0;
                tamperDefSlider.row.querySelector('.wg-slider').value = found.true_defense || 0;
                tamperDefSlider.row.querySelector('input[type="number"]').value = found.true_defense || 0;
                tamperCountInput.value = found.count || 1;
                tamperRarityInput.value = found.rarity_rank || 'C';
                tamperRarityGrid.querySelectorAll('.wg-rarity-btn').forEach(b => {
                    b.classList.remove('active');
                    if (b.dataset.r === tamperRarityInput.value) b.classList.add('active');
                });
                tamperLog.innerText = `✓ "${found.title}" loaded`;
            } else {
                loadedCardData = null;
                tamperLog.innerText = '⚠ No owned card found';
            }
        } catch (e) { tamperLog.innerText = '✗ ' + e; }
    };
    tamperBtn.onclick = async () => {
        if (!loadedCardData) { tamperLog.innerText = '⚠ Please load a card first'; return; }
        tamperLog.innerText = 'Tampering...';
        try {
            let imgData = loadedCardData.card.image_url;
            if (tamperImage.files && tamperImage.files[0]) {
                const file = tamperImage.files[0];
                imgData = await new Promise(resolve => {
                    const reader = new FileReader();
                    reader.onload = e => resolve(e.target.result);
                    reader.readAsDataURL(file);
                });
            }
            const db = await new Promise((resolve, reject) => {
                const req = indexedDB.open('wiki-gacha-db');
                req.onerror = e => reject('DB Error: ' + e.target.error);
                req.onsuccess = e => resolve(e.target.result);
            });
            const tx = db.transaction([loadedCardData.store], 'readwrite');
            const store = tx.objectStore(loadedCardData.store);
            const oldId = loadedCardData.card.id;
            const newId = Math.floor(Math.random() * 1e8) + 1e8;
            const card = { ...loadedCardData.card };
            card.id = newId;
            if (tamperTitle.value.trim()) card.title = tamperTitle.value.trim();
            card.extract = tamperDescription.value.trim() || '';
            card.abstract = tamperDescription.value.trim() || '';
            card.flavor_text = tamperFlavorText.value.trim() || '';
            card.image_url = imgData;
            card.true_attack = tamperAtkSlider.getValue();
            card.true_defense = tamperDefSlider.getValue();
            card.count = parseInt(tamperCountInput.value) || 1;
            card.rarity_rank = tamperRarityInput.value;

            await new Promise((resolve, reject) => {
                const delReq = store.delete(oldId);
                delReq.onsuccess = () => resolve();
                delReq.onerror = ev => reject(ev.target.error);
            });

            await new Promise((resolve, reject) => {
                const putReq = store.put(card);
                putReq.onsuccess = () => resolve();
                putReq.onerror = ev => reject(ev.target.error);
            });
            db.close();
            loadedCardData.card = card;
            tamperLog.innerText = `✓ Tamper complete!`;

            tamperIdInput.value = '';
            tamperTitle.value = '';
            tamperImage.value = '';
            tamperDescription.value = '';
            tamperFlavorText.value = '';
            tamperAtkSlider.row.querySelector('.wg-slider').value = 0;
            tamperAtkSlider.row.querySelector('input[type="number"]').value = 0;
            tamperDefSlider.row.querySelector('.wg-slider').value = 0;
            tamperDefSlider.row.querySelector('input[type="number"]').value = 0;
            tamperCountInput.value = 1;
            tamperRarityInput.value = 'SR';
            tamperRarityGrid.querySelectorAll('.wg-rarity-btn').forEach(b => {
                b.classList.toggle('active', b.dataset.r === 'SR');
            });
            loadedCardData = null;
        } catch (e) { tamperLog.innerText = '✗ ' + e; }
    };
    const sec5 = document.createElement('div');
    sec5.className = 'wg-section';
    sections.push(sec5);
    sec5.innerHTML = `<div class="wg-section-title">Unlock All Trophies</div>`;
    const unlockTrophiesBtn = document.createElement('button');
    unlockTrophiesBtn.className = 'wg-btn wg-btn-yellow wg-btn-full';
    unlockTrophiesBtn.style.background = '#d97706';
    unlockTrophiesBtn.innerText = '🏆 Unlock All Achievements';
    const trophyLog = document.createElement('div');
    trophyLog.className = 'wg-log';
    sec5.appendChild(unlockTrophiesBtn);
    sec5.appendChild(trophyLog);
    body.appendChild(sec5);
    unlockTrophiesBtn.onclick = async () => {
        trophyLog.innerText = 'Processing...';
        try {
            const ALL_TROPHIES = [
                "beginner_luck", "gacha_addict", "routine", "whale", "leviathan",
                "collector", "curator", "collection_5000", "dust_collector", "shiny",
                "super_rare", "ultra_luck", "legend", "desire_sensor", "god_whim",
                "double_rainbow", "miracle", "rainbow", "full_house", "all_uc",
                "dupe_2", "dupe_3", "dupe_5", "elite", "legendary_vault", "glass_cannon",
                "fortress", "heavy_hitter", "iron_wall", "perfect_being", "quality_zero",
                "weakest", "origin", "lucky_seven", "long_winded", "minimalist",
                "katakana", "mirror", "step", "ads",
                "raid_clear_1", "raid_clear_3", "raid_clear_5", "raid_clear_10"
            ];
            const db = await new Promise((resolve, reject) => {
                const req = indexedDB.open('wiki-gacha-db');
                req.onerror = e => reject('DB Error: ' + e.target.error);
                req.onsuccess = e => resolve(e.target.result);
            });
            if (!db.objectStoreNames.contains('user_data')) {
                db.close();
                trophyLog.innerText = '⚠ No save data found';
                return;
            }
            const tx = db.transaction(['user_data'], 'readwrite');
            const store = tx.objectStore('user_data');
            await new Promise((resolve, reject) => {
                let pending = 2;
                const checkDone = () => { if (--pending === 0) resolve(); };
                const putJp = store.put(ALL_TROPHIES, 'jp:trophies');
                putJp.onsuccess = checkDone;
                putJp.onerror = ev => reject(ev.target.error);
                const putEn = store.put(ALL_TROPHIES, 'en:trophies');
                putEn.onsuccess = checkDone;
                putEn.onerror = ev => reject(ev.target.error);
            });
            db.close();
            trophyLog.innerText = '✓ All trophies unlocked! Please reload to confirm.';
        } catch (e) {
            trophyLog.innerText = '✗ ' + e;
        }
    };

    async function doAutoCompLoop() {
        if (!autoCompRunning) return;
        const startId = parseInt(document.getElementById('comp-start-id').value) || 1;
        const endId = parseInt(document.getElementById('comp-end-id').value) || 1500000;
        const lang = document.getElementById('comp-lang').value;
        const log = document.getElementById('comp-log');
        const statsEl = document.getElementById('comp-stats');

        if (autoCompStats.currentId < startId) autoCompStats.currentId = startId;

        try {
            const db = await new Promise((resolve, reject) => {
                const req = indexedDB.open('wiki-gacha-db');
                req.onsuccess = e => resolve(e.target.result);
                req.onerror = e => reject(e);
            });
            const storeName = lang === 'JP' ? 'cards_jp' : 'cards_en';
            const ownedIds = new Set();
            if (db.objectStoreNames.contains(storeName)) {
                const tx = db.transaction([storeName], 'readonly');
                const store = tx.objectStore(storeName);
                const allKeys = await new Promise(res => {
                    const req = store.getAllKeys();
                    req.onsuccess = () => res(req.result);
                });
                allKeys.forEach(id => ownedIds.add(id));
            }
            db.close();

            const CONCURRENCY = 3;
            while (autoCompRunning && autoCompStats.currentId <= endId) {
                const batch = [];
                for (let i = 0; i < CONCURRENCY && autoCompStats.currentId <= endId; i++) {
                    const id = autoCompStats.currentId++;
                    if (ownedIds.has(id)) {
                        autoCompStats.skipped++;
                        continue;
                    }
                    batch.push(id);
                }

                if (batch.length > 0) {
                    await Promise.all(batch.map(async (targetId) => {
                        try {
                            const res = await fetch(`/api/card?id=${targetId}&lang=${lang}`);
                            if (res.ok) {
                                const data = await res.json();
                                const card = data.card || data;
                                if (card?.id) {
                                    await saveCardsToDB([card], lang);
                                    autoCompStats.added++;
                                } else { autoCompStats.failed++; }
                            } else { autoCompStats.failed++; }
                        } catch (e) { autoCompStats.failed++; }
                    }));
                }

                statsEl.innerHTML = `
                    <div class="wg-stat"><span class="wg-stat-key">Added</span><span class="wg-stat-val">${autoCompStats.added}</span></div>
                    <div class="wg-stat"><span class="wg-stat-key">Skipped</span><span class="wg-stat-val">${autoCompStats.skipped}</span></div>
                    <div class="wg-stat"><span class="wg-stat-key">Failed</span><span class="wg-stat-val">${autoCompStats.failed}</span></div>
                    <div class="wg-stat"><span class="wg-stat-key">Current ID</span><span class="wg-stat-val">${autoCompStats.currentId}</span></div>
                `;
                log.innerText = `ID: ${autoCompStats.currentId} Processing...`;
                await new Promise(r => setTimeout(r, 100));
            }

            if (autoCompStats.currentId > endId) {
                autoCompRunning = false;
                const btn = document.getElementById('auto-comp-btn');
                if (btn) {
                    btn.className = 'wg-btn wg-btn-green wg-btn-full';
                    btn.innerText = '▶ Start Complete';
                }
                log.innerText = 'Completed!';
                alert('Complete process finished.');
            }

        } catch (e) {
            log.innerText = 'Error: ' + e.message;
            autoCompRunning = false;
            const btn = document.getElementById('auto-comp-btn');
            if (btn) {
                btn.className = 'wg-btn wg-btn-green wg-btn-full';
                btn.innerText = '▶ コンプ開始';
            }
        }
    }

    const sec6 = document.createElement('div');
    sec6.className = 'wg-section';
    sections.push(sec6);
    sec6.innerHTML = `
        <div class="wg-section-title">Card Complete</div>
        <div class="wg-row">
            <span class="wg-label">Range</span>
            <input type="number" id="comp-start-id" class="wg-input" value="1" style="width:70px">
            <span class="wg-label">~</span>
            <input type="number" id="comp-end-id" class="wg-input" value="1500000" style="width:70px">
        </div>
        <div class="wg-row">
            <span class="wg-label">Language</span>
            <select id="comp-lang" class="wg-select">
                <option value="JP">Japanese</option>
                <option value="EN">English</option>
            </select>
        </div>
        <div id="comp-stats" class="wg-stats" style="margin-bottom:6px">
            <div class="wg-stat"><span class="wg-stat-key">Added</span><span class="wg-stat-val">0</span></div>
            <div class="wg-stat"><span class="wg-stat-key">Skipped</span><span class="wg-stat-val">0</span></div>
            <div class="wg-stat"><span class="wg-stat-key">Failed</span><span class="wg-stat-val">0</span></div>
            <div class="wg-stat"><span class="wg-stat-key">Current ID</span><span class="wg-stat-val">1</span></div>
        </div>
        <button id="auto-comp-btn" class="wg-btn wg-btn-green wg-btn-full">▶ Start Complete</button>
        <div id="comp-log" class="wg-log">Waiting...</div>
    `;
    body.appendChild(sec6);
    const compBtn = sec6.querySelector('#auto-comp-btn');
    compBtn.onclick = () => {
        autoCompRunning = !autoCompRunning;
        if (autoCompRunning) {
            compBtn.className = 'wg-btn wg-btn-red wg-btn-full';
            compBtn.innerText = '■ Stop';
            doAutoCompLoop();
        } else {
            compBtn.className = 'wg-btn wg-btn-green wg-btn-full';
            compBtn.innerText = '▶ Start Complete';
            document.getElementById('comp-log').innerText = 'Stopped.';
        }
    };
    const toggleBtn = document.createElement('button');
    toggleBtn.id = 'wgcm-toggle';
    toggleBtn.innerText = '⚡';

    let tDragOX = 0, tDragOY = 0, tDragging = false;
    let tHasMoved = false;
    toggleBtn.addEventListener('mousedown', e => {
        tDragging = true;
        tHasMoved = false;
        tDragOX = e.clientX - toggleBtn.getBoundingClientRect().left;
        tDragOY = e.clientY - toggleBtn.getBoundingClientRect().top;
        toggleBtn.style.cursor = 'grabbing';
    });
    document.addEventListener('mousemove', e => {
        if (!tDragging) return;
        tHasMoved = true;
        toggleBtn.style.right = 'auto';
        toggleBtn.style.left = (e.clientX - tDragOX) + 'px';
        toggleBtn.style.top = (e.clientY - tDragOY) + 'px';
    });
    document.addEventListener('mouseup', () => {
        if (!tDragging) return;
        tDragging = false;
        toggleBtn.style.cursor = 'pointer';
    });
    let panelVisible = true;
    toggleBtn.addEventListener('click', (e) => {

        if (tHasMoved) {
            e.preventDefault();
            return;
        }
        panelVisible = !panelVisible;
        wrap.style.display = panelVisible ? 'block' : 'none';
    });
    document.body.appendChild(toggleBtn);
    document.body.appendChild(wrap);
})();