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);
})();