Easy tool for WikiGacha
// ==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);
})();