// ==UserScript== // @name GGn Video Games by Genre - Search Helper // @namespace feng9148scripts // @version 1.3 // @author feng9148 // @description GGg Forum Game "Video Games by Genre" helper. Makes searching and replying much easier. // @match https://gazellegames.net/forums.php* // @match https://gazellegames.net/torrents.php* // @grant none // @license MIT // ==/UserScript== // https://gazellegames.net/forums.php?action=viewthread&threadid=35657&page=1#post4235911 (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 url = `https://gazellegames.net/torrents.php?id=${idMatch[1]}`; const bbcode = `[torrent]${url}[/torrent]`; 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(); } /////// Inject panel function inject() { const afterNode = document.getElementById('quickreplytext'); if (!afterNode) return; const textarea = document.getElementById('quickpost'); const panel = document.createElement('div'); panel.style.cssText = 'background: #111e29; padding: 8px 10px; margin: 4px 0; text-align: center;'; const title = document.createElement('div'); title.textContent = 'Find Video Games by Genre'; title.style.cssText = 'font-size: 11px; color: #aaa; margin-bottom: 5px;'; // --------------------------------------------------------------------- // 新增:快速粘贴识别行 (Quick Paste Row) // --------------------------------------------------------------------- const pasteRow = document.createElement('div'); pasteRow.style.cssText = 'margin-bottom: 6px; display: flex; justify-content: center; align-items: center; gap: 6px;'; const pasteInput = document.createElement('input'); pasteInput.type = 'text'; pasteInput.placeholder = 'Paste "Genre 2010-2015" here...'; pasteInput.style.cssText = 'width: 250px; padding: 1px 5px; height: 22px; box-sizing: border-box; font-size: 11px;'; const pasteBtn = document.createElement('input'); pasteBtn.type = 'button'; pasteBtn.value = 'Clipboard Auto-Fill'; pasteBtn.title = 'Read from clipboard and auto fill below'; pasteBtn.style.cssText = 'height: 22px; font-size: 11px; cursor: pointer;'; pasteRow.append(pasteInput, pasteBtn); // --------------------------------------------------------------------- // Search row // --------------------------------------------------------------------- const searchRow = document.createElement('div'); searchRow.style.cssText = 'display: inline-flex; align-items: center; gap: 6px;'; const makeInput = (placeholder, width, type) => { const el = document.createElement('input'); el.type = type || 'text'; el.placeholder = placeholder; el.style.cssText = `width: ${width}; padding: 1px 2px 1px 5px; height: 20px; box-sizing: border-box;`; return el; }; const genreInput = makeInput('Genre', '140px'); const yearFrom = makeInput('From', '62px', 'number'); const yearTo = makeInput('To', '62px', 'number'); yearFrom.min = yearTo.min = '1975'; yearFrom.max = yearTo.max = String(new Date().getFullYear()); const dash = document.createElement('span'); dash.textContent = '–'; dash.style.cssText = 'color: #aaa;'; const searchBtn = document.createElement('input'); searchBtn.type = 'button'; searchBtn.value = 'Search Games'; const note = document.createElement('span'); note.textContent = ''; note.style.cssText = 'font-size: 11px; color: #aaa;'; // 统一填充解析结果的逻辑 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; } // 1. 初始化时自动解析最后回复 const lp = getLastPost(); if (lp && lp.text) { applyParsedText(lp.text); } // 2. 监听 Genre 输入框直接粘贴文本时的解析处理 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); // 3. 点击 Clipboard Auto-Fill 按钮直接读取剪贴板 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'); }); searchRow.append(genreInput, yearFrom, dash, yearTo, searchBtn, note); // Generate row const genRow = document.createElement('div'); genRow.style.cssText = 'margin-top: 6px;'; const genBtn = document.createElement('input'); genBtn.type = 'button'; genBtn.value = 'Generate Genre/Year Combo'; 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 + '\n\n' + combo; textarea.focus(); textarea.setSelectionRange(textarea.value.length, textarea.value.length); }); genRow.appendChild(genBtn); panel.append(title, pasteRow, searchRow, genRow); afterNode.after(panel); } inject(); })();