// ==UserScript== // @name GGn Video Games by Genre - Search Helper (One-Click Auto Edition) // @namespace feng9148scripts // @version 1.6 // @author feng9148 (Modified) // @description GGn Forum Game "Video Games by Genre" helper with One-Click background fetch & auto-fill. // @match https://gazellegames.net/forums.php* // @match https://gazellegames.net/torrents.php* // @grant none // @license MIT // ==/UserScript== (function () { 'use strict'; // ========================================================================= // SEARCH RESULTS PAGE — active when ?vgbg=1 is present // ========================================================================= if (location.pathname === '/torrents.php') { if (!new URLSearchParams(location.search).has('vgbg')) return; const style = document.createElement('style'); style.textContent = ` .vgbg-copy { margin-left: 6px; padding: 0 6px; height: 16px; line-height: 16px; font-size: 11px; font-family: Arial, sans-serif; background: #2b4e66; color: #fff; border: none; border-radius: 3px; cursor: pointer; vertical-align: middle; } .vgbg-copy.done { background: #2b6640; } tr.group.vgbg-hidden, tr.group_torrent.vgbg-hidden { display: none; } `; document.head.appendChild(style); function process() { for (const row of document.querySelectorAll('tr.group:not([data-vgbg])')) { row.dataset.vgbg = '1'; const ratingSpan = row.querySelector('span#grouprating'); if (ratingSpan && ratingSpan.textContent.includes('[18+]')) { row.classList.add('vgbg-hidden'); const idMatch = row.className.match(/\bgroup_(\d+)\b/); if (idMatch) { for (const child of document.querySelectorAll(`tr.group_torrent.groupid_${idMatch[1]}`)) { child.classList.add('vgbg-hidden'); } } continue; } const idMatch = row.className.match(/\bgroup_(\d+)\b/); if (!idMatch) continue; const nameLink = row.querySelector('span#groupname a'); if (!nameLink) continue; const groupName = nameLink.textContent.trim(); const yearEl = row.querySelector('#groupyear'); const groupYear = yearEl ? yearEl.textContent.trim() : ''; // 同步改为 ByGenre 的标准格式 const bbcode = `[url=https://gazellegames.net/torrents.php?id=${idMatch[1]}]${groupName} ${groupYear}[/url]`; const btn = document.createElement('button'); btn.type = 'button'; btn.className = 'vgbg-copy'; btn.textContent = 'click to copy'; btn.title = bbcode; btn.addEventListener('click', e => { e.preventDefault(); function done() { btn.textContent = '✓ copied!'; btn.classList.add('done'); setTimeout(() => { btn.textContent = 'click to copy'; btn.classList.remove('done'); }, 1500); } if (navigator.clipboard) { navigator.clipboard.writeText(bbcode).then(done).catch(fallback); } else { fallback(); } function fallback() { const ta = document.createElement('textarea'); ta.value = bbcode; ta.style.cssText = 'position:fixed;opacity:0'; document.body.appendChild(ta); ta.select(); document.execCommand('copy'); ta.remove(); done(); } }); nameLink.after(btn); } } process(); new MutationObserver(process).observe(document.body, { childList: true, subtree: true }); return; } // ========================================================================= // FORUM THREAD PAGE // ========================================================================= const THREAD_ID = '14895'; const threadInput = document.querySelector('input[name="thread"]'); if (!threadInput || threadInput.value !== THREAD_ID) return; //////// Genre → GGn tag(s) mapping const GENRE_TAGS = [ [/\bfps\b|first.?person.?shoot/i, ['shooter', 'first.person']], [/\bshoot.?em.?up\b|\bshmup\b/i, ['shoot.em.up']], [/\bbullet.?hell\b/i, ['bullet.hell']], [/\bbeat.?em.?up\b/i, ['beat.em.up']], [/\bhack.?and.?slash\b/i, ['hack.and.slash']], [/\breal.?time.?strat/i, ['real.time.strategy']], [/\bturn.?based.?strat/i, ['turn.based.strategy']], [/\b(vehicle|flight|space).?sim/i, ['vehicle.simulation', 'flight.simulation', 'space.simulation']], [/\bvisual.?novel\b/i, ['visual.novel']], [/\btext.?adventure\b/i, ['text.adventure']], [/\binteractive.?fiction\b/i, ['interactive.fiction']], [/\btower.?de[fe][en][cn][sc]e\b/i, ['tower.defense']], [/\bbattle.?royale\b/i, ['battle.royale']], [/\bcard.?game\b/i, ['card.game']], [/\bboard.?game\b/i, ['board.game']], [/\bpoint.?and.?click\b/i, ['adventure', 'point.and.click']], [/\brts\b/i, ['real.time.strategy']], [/\btbs\b/i, ['turn.based.strategy']], [/\brpg\b|role.?playing/i, ['role.playing']], [/\bmetroidvania\b/i, ['metroidvania']], [/\broguelite\b/i, ['roguelite']], [/\broguelike\b/i, ['roguelike']], [/\bplatform(?:er)?\b/i, ['platformer']], [/\bpuzzle\b/i, ['puzzle']], [/\brhythm\b/i, ['rhythm']], [/\bfight(?:ing)?\b/i, ['fighting']], [/\bsurvival\b/i, ['survival']], [/\bstealth\b/i, ['stealth']], [/\bracing\b/i, ['racing']], [/\bhorror\b/i, ['horror']], [/\bsport[s]?\b/i, ['sports']], [/\baction\b/i, ['action']], [/\bstrategy\b/i, ['strategy']], [/\bsimulat/i, ['simulation']], [/\badventure\b/i, ['adventure']], [/\bidle\b|\bclicker\b/i, ['idle']], [/\bmanagement\b/i, ['management']], [/\bparty\b/i, ['party']], [/\btrivia\b/i, ['trivia']], [/\bshooter\b/i, ['shooter']], [/\barcade\b/i, ['arcade']], [/\btactics\b/i, ['tactics']], [/\b4[xX]\b/i, ['4x']], [/\bwargame\b/i, ['wargame']], [/\bgrand.?strat/i, ['grand.strategy']], [/\bdungeon.?crawl/i, ['dungeon.crawler']], [/\bsouls.?like\b/i, ['souls.like']], [/\bgod.?game\b/i, ['god.game']], [/\btycoon\b/i, ['tycoon']], [/\bcity.?build/i, ['city.building']], [/\bdeck.?build/i, ['deck.building']], [/\bcard.?battler\b/i, ['card.battler']], [/\bauto.?battler\b/i, ['auto.battler']], [/\bhidden.?object\b/i, ['hidden.object']], [/\bmatch.?3\b/i, ['match.3']], [/\bpinball\b/i, ['pinball']], [/\brail.?shoot/i, ['rail.shooter']], [/\btwin.?stick/i, ['twin.stick']], [/\bgraphic.?adventure\b/i, ['graphic.adventure']], [/\blife.?sim/i, ['life.simulation']], [/\bdating.?sim/i, ['dating.simulation']], [/\bbusiness.?sim/i, ['business.simulation']], [/\bconstruction.?sim/i, ['construction.simulation']], [/\bwalking.?sim/i, ['walking.simulation']], [/\binteractive.?movie\b/i, ['interactive.movie']], [/\bfull.?motion.?video\b|\bfmv\b/i, ['full.motion.video']], [/\bcreature.?collect/i, ['creature.collector']], ]; function getTagsForGenre(genre) { for (const [re, tags] of GENRE_TAGS) { if (re.test(genre)) return tags; } return [genre.toLowerCase().trim().replace(/\s+/g, '.')]; } //////// Genre bank const GENRE_BANK = [ 'Action', 'Adventure', 'Beat em Up', 'Board Game', 'Bullet Hell', 'Card Game', 'Fighting', 'Horror', 'Idle', 'Management', 'Metroidvania', 'Party', 'Platformer', 'Puzzle', 'Racing', 'Rhythm', 'Roguelike', 'Roguelite', 'RPG', 'Shoot em Up', 'Shooter', 'Simulation', 'Sports', 'Stealth', 'Strategy', 'Survival', 'Tower Defense', 'Turn-Based Strategy', 'Real-Time Strategy', 'Visual Novel', 'Hack and Slash', ]; ///////// Parse last post function getLastPost() { const posts = Array.from(document.querySelectorAll('[id^="post"]')) .filter(p => !p.classList.contains('sticky_post')); if (!posts.length) return null; const last = posts[posts.length - 1]; const body = last.querySelector('td.body'); return { el: last, text: body ? body.innerText.trim() : '' }; } function parsePrompt(text) { const lines = text.split('\n') .map(l => l.trim()) .filter(l => l && !/^last edited by\b/i.test(l)); if (!lines.length) return null; let promptLine = null; for (let i = lines.length - 1; i >= 0; i--) { if (/\d{4}/.test(lines[i])) { promptLine = lines[i]; break; } } if (!promptLine) return null; const rangeM = promptLine.match(/(\d{4})\s*[-–]\s*(\d{4})/); const openM = promptLine.match(/(\d{4})\s*[-–]\s*\)/); const singleM = promptLine.match(/\(?\s*(\d{4})\s*\)?/); let yearStart, yearEnd; if (rangeM) { yearStart = +rangeM[1]; yearEnd = +rangeM[2]; } else if (openM) { yearStart = +openM[1]; yearEnd = null; } else if (singleM) { yearStart = yearEnd = +singleM[1]; } else { return null; } const genre = promptLine.replace(/[,(\s]\s*\d{4}\s*[-–]?\s*\d{0,4}\s*\)?.*$/, '').trim(); if (!genre) return null; return { genre, yearStart, yearEnd }; } ////////// Genre/year generator function getRecentGenres(n) { return Array.from(document.querySelectorAll('[id^="post"]')) .filter(p => !p.classList.contains('sticky_post')) .slice(-(n || 15)) .map(p => { const body = p.querySelector('td.body'); if (!body) return null; const lines = body.innerText.trim().split('\n') .map(l => l.trim()) .filter(l => l && !/^last edited by\b/i.test(l)); for (let i = lines.length - 1; i >= 0; i--) { if (/\d{4}/.test(lines[i])) { return lines[i].replace(/[,(\s]\s*\d{4}.*$/, '').trim().toLowerCase() || null; } } return null; }) .filter(Boolean); } function pickGenre(recentGenres) { const recentSet = new Set(recentGenres.map(g => g.toLowerCase())); const available = GENRE_BANK.filter(g => !recentSet.has(g.toLowerCase())); const pool = available.length ? available : GENRE_BANK; return pool[Math.floor(Math.random() * pool.length)]; } function pickYearRange(genre) { const now = new Date().getFullYear(); const lengths = [4, 4, 5, 5, 6, 6, 7, 7, 8]; const len = lengths[Math.floor(Math.random() * lengths.length)]; let minStart = 1990; if (genre && genre.toLowerCase() === 'tower defense') { minStart = 2007; } const maxStart = now - len + 1; const safeMinStart = minStart > maxStart ? maxStart : minStart; const start = safeMinStart + Math.floor(Math.random() * (maxStart - safeMinStart + 1)); return { start, end: start + len - 1 }; } function formatCombo(genre, start, end) { return `${genre} ${start}-${end}`; } /////// Build search URL function buildSearchUrl(genre, yearStart, yearEnd) { const tags = getTagsForGenre(genre); const yearParam = yearStart ? (yearEnd && yearEnd !== yearStart ? `${yearStart}-${yearEnd}` : String(yearStart)) : null; const p = new URLSearchParams({ action: 'advanced', taglist: tags.join(','), tags_type: '0', order_by: 's3', order_way: 'desc', 'filter_cat[1]': '1', vgbg: '1', }); if (yearParam) p.set('year', yearParam); return 'https://gazellegames.net/torrents.php?' + p.toString(); } /////// Background Fetch & Parse (核心后台抓取与格式化逻辑) function getRecentGameIds(n = 15) { const ids = new Set(); const posts = Array.from(document.querySelectorAll('[id^="post"]')) .filter(p => !p.classList.contains('sticky_post')) .slice(-n); for (const post of posts) { const body = post.querySelector('td.body'); if (!body) continue; for (const a of body.querySelectorAll('a[href*="torrents.php"]')) { const match = a.href.match(/[?&]id=(\d+)/); if (match) ids.add(match[1]); } const textMatches = body.innerText.matchAll(/torrents\.php\?[^ \n\r\t"'\]]*?id=(\d+)/g); for (const match of textMatches) { ids.add(match[1]); } } return ids; } async function fetchGameByGenre(genre, yearStart, yearEnd) { const url = buildSearchUrl(genre, yearStart, yearEnd); const resp = await fetch(url, { credentials: 'same-origin' }); if (!resp.ok) throw new Error(`HTTP ${resp.status}`); const html = await resp.text(); const doc = new DOMParser().parseFromString(html, 'text/html'); const recentGameIds = getRecentGameIds(15); const validGames = []; for (const row of doc.querySelectorAll('tr.group')) { // 1. 过滤 [18+] 游戏 const ratingSpan = row.querySelector('span#grouprating'); if (ratingSpan && ratingSpan.textContent.includes('[18+]')) { continue; } // 2. 提取 Torrent Group ID、游戏名、年份 (对应 Torrent Game Copier 的 ByGenre 逻辑) const displayEl = row.querySelector('#displayname') || row; const linkElement = displayEl.querySelector('#groupname a'); if (!linkElement) continue; const groupName = linkElement.textContent.trim(); const yearEl = displayEl.querySelector('#groupyear'); const groupYear = yearEl ? yearEl.textContent.trim() : ''; let torrentId = null; const href = linkElement.getAttribute('href') || ''; if (href.includes('?')) { torrentId = new URLSearchParams(href.split('?')[1]).get('id'); } if (!torrentId) { const idMatch = row.className.match(/\bgroup_(\d+)\b/); if (idMatch) torrentId = idMatch[1]; } // 3. 排除最近 15 楼已经回答过的游戏 ID if (!torrentId || recentGameIds.has(String(torrentId))) { continue; } if (groupName) { // 按照 Torrent Game Copier 的 ByGenre 格式拼接 const formatted = `[url=https://gazellegames.net/torrents.php?id=${torrentId}]${groupName} ${groupYear}[/url]`; validGames.push(formatted); } } if (!validGames.length) return null; // 从第一页符合条件的结果中随机挑一个(如果只想固定挑第一个,可改为 return validGames[0];) return validGames[Math.floor(Math.random() * validGames.length)]; } /////// Inject panel function inject() { const afterNode = document.getElementById('quickreplytext'); if (!afterNode) return; const textarea = document.getElementById('quickpost'); const uiStyle = document.createElement('style'); uiStyle.textContent = ` .vgbg-panel { background: #111e29; border: 1px solid #1c3245; border-radius: 6px; padding: 10px 14px; margin: 8px 0; display: flex; flex-direction: column; align-items: center; gap: 8px; font-family: Arial, sans-serif; } .vgbg-title { font-size: 11px; color: #8da6b8; font-weight: bold; letter-spacing: 0.5px; } .vgbg-row { display: flex; align-items: center; justify-content: space-between; gap: 6px; width: 420px; } .vgbg-input { height: 26px; padding: 0 8px; background: #182a3a; border: 1px solid #294863; border-radius: 4px; color: #fff; font-size: 12px; box-sizing: border-box; } .vgbg-input:focus { border-color: #4384b5; outline: none; } .vgbg-btn { height: 26px; padding: 0 12px; background: #2b4e66; border: 1px solid #36617f; border-radius: 4px; color: #fff; font-size: 11px; font-weight: bold; cursor: pointer; white-space: nowrap; transition: background 0.15s ease, border-color 0.15s ease; } .vgbg-btn:hover { background: #345f7c; border-color: #4a80a6; } .vgbg-btn:disabled { opacity: 0.6; cursor: not-allowed; } .vgbg-btn-generate { width: 100%; background: #1f394c; } .vgbg-btn-generate:hover { background: #2b4e66; } .vgbg-btn-oneclick { width: 100%; height: 30px; background: #235c3b; border-color: #2e7a4e; font-size: 12px; } .vgbg-btn-oneclick:hover { background: #2b7048; border-color: #3b9c64; } .vgbg-dash { color: #708b9e; font-weight: bold; } `; document.head.appendChild(uiStyle); const panel = document.createElement('div'); panel.className = 'vgbg-panel'; const title = document.createElement('div'); title.className = 'vgbg-title'; title.textContent = 'FIND VIDEO GAMES BY GENRE'; // Row 1: 快速识别行 const pasteRow = document.createElement('div'); pasteRow.className = 'vgbg-row'; const pasteInput = document.createElement('input'); pasteInput.type = 'text'; pasteInput.className = 'vgbg-input'; pasteInput.placeholder = 'Paste "Genre 2010-2015" here...'; pasteInput.style.flex = '1'; const pasteBtn = document.createElement('input'); pasteBtn.type = 'button'; pasteBtn.className = 'vgbg-btn'; pasteBtn.value = 'Clipboard Auto-Fill'; pasteRow.append(pasteInput, pasteBtn); // Row 2: 搜索字段与操作行 const searchRow = document.createElement('div'); searchRow.className = 'vgbg-row'; const genreInput = document.createElement('input'); genreInput.type = 'text'; genreInput.className = 'vgbg-input'; genreInput.placeholder = 'Genre'; genreInput.style.flex = '1'; const makeYearInput = (placeholder) => { const el = document.createElement('input'); el.type = 'number'; el.className = 'vgbg-input'; el.placeholder = placeholder; el.style.width = '62px'; el.style.textAlign = 'center'; el.min = '1975'; el.max = String(new Date().getFullYear()); return el; }; const yearFrom = makeYearInput('From'); const yearTo = makeYearInput('To'); const dash = document.createElement('span'); dash.className = 'vgbg-dash'; dash.textContent = '–'; const searchBtn = document.createElement('input'); searchBtn.type = 'button'; searchBtn.className = 'vgbg-btn'; searchBtn.value = 'Search Games'; searchBtn.style.width = '110px'; searchRow.append(genreInput, yearFrom, dash, yearTo, searchBtn); // Row 3: 原本的单独生成下一题按钮 const genRow = document.createElement('div'); genRow.className = 'vgbg-row'; const genBtn = document.createElement('input'); genBtn.type = 'button'; genBtn.className = 'vgbg-btn vgbg-btn-generate'; genBtn.value = 'Generate Genre / Year Combo Only'; genRow.appendChild(genBtn); // Row 4: 新增的一键全自动按钮 const oneClickRow = document.createElement('div'); oneClickRow.className = 'vgbg-row'; const oneClickBtn = document.createElement('input'); oneClickBtn.type = 'button'; oneClickBtn.className = 'vgbg-btn vgbg-btn-oneclick'; oneClickBtn.value = '⚡ One-Click Auto Fill (一键生成按钮测试版)'; oneClickRow.appendChild(oneClickBtn); // 解析与自动填充核心逻辑 function applyParsedText(text) { if (!text) return false; const parsed = parsePrompt(text); if (parsed) { genreInput.value = parsed.genre; yearFrom.value = parsed.yearStart; yearTo.value = parsed.yearEnd != null ? parsed.yearEnd : ''; return true; } return false; } const lp = getLastPost(); if (lp && lp.text) { applyParsedText(lp.text); } const handlePasteEvent = e => { const clipboardData = e.clipboardData || window.clipboardData; if (!clipboardData) return; const pastedText = clipboardData.getData('text'); if (applyParsedText(pastedText)) { e.preventDefault(); } }; genreInput.addEventListener('paste', handlePasteEvent); pasteInput.addEventListener('paste', handlePasteEvent); pasteBtn.addEventListener('click', async () => { if (navigator.clipboard && navigator.clipboard.readText) { try { const text = await navigator.clipboard.readText(); if (applyParsedText(text)) { pasteInput.value = text; } else { alert('Could not recognize Genre/Year pattern in clipboard text.'); } } catch (err) { alert('Failed to read clipboard. Please paste manually into the text box.'); } } else { alert('Clipboard API not supported. Please paste manually.'); } }); searchBtn.addEventListener('click', () => { const genre = genreInput.value.trim(); if (!genre) { genreInput.focus(); return; } const ys = parseInt(yearFrom.value) || null; const ye = parseInt(yearTo.value) || null; window.open(buildSearchUrl(genre, ys, ye), '_blank', 'noopener'); }); genBtn.addEventListener('click', () => { if (!textarea) return; const selectedGenre = pickGenre(getRecentGenres(15)); const years = pickYearRange(selectedGenre); const combo = formatCombo(selectedGenre, years.start, years.end); textarea.value = (textarea.value ? textarea.value.trimEnd() + '\n\n' : '') + combo; textarea.focus(); textarea.setSelectionRange(textarea.value.length, textarea.value.length); }); // 一键全自动事件处理 oneClickBtn.addEventListener('click', async () => { if (!textarea) return; const genre = genreInput.value.trim(); if (!genre) { genreInput.focus(); return; } const ys = parseInt(yearFrom.value) || null; const ye = parseInt(yearTo.value) || null; const originalText = oneClickBtn.value; oneClickBtn.disabled = true; oneClickBtn.value = '⏳ Searching & generating in background...'; try { // 1 & 2. 后台请求搜索页并提取 ByGenre 格式链接 const byGenreBBCode = await fetchGameByGenre(genre, ys, ye); if (!byGenreBBCode) { alert(`未找到符合条件的游戏: ${genre} (${ys || ''}-${ye || ''}),请手动修改条件或点击 Search Games 查看。`); return; } // 3. 生成新的 Genre / Year Combo 并填入回复框 const selectedGenre = pickGenre(getRecentGenres(15)); const years = pickYearRange(selectedGenre); const combo = formatCombo(selectedGenre, years.start, years.end); // 直接覆写进回复框(若连续点击可自动换一个游戏和题目) textarea.value = `${byGenreBBCode}\n\n${combo}`; textarea.focus(); textarea.setSelectionRange(textarea.value.length, textarea.value.length); oneClickBtn.value = '✓ Done! (Click again to re-roll)'; setTimeout(() => { oneClickBtn.value = originalText; }, 2000); } catch (err) { console.error(err); alert('后台请求失败: ' + err.message); oneClickBtn.value = originalText; } finally { oneClickBtn.disabled = false; } }); panel.append(title, pasteRow, searchRow, genRow, oneClickRow); afterNode.after(panel); } inject(); })();