// ==UserScript== // @name GGn Video Games by Genre - Search Helper // @namespace feng9148scripts // @version 1.4 // @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'); // 注入专属 CSS UI 样式表 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-generate { width: 100%; /* 生成按钮默认铺满 420px 宽度 */ background: #1f394c; } .vgbg-btn-generate:hover { background: #2b4e66; } .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: 快速识别行 (总宽度 420px) // --------------------------------------------------------------------- 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'; pasteBtn.title = 'Read from clipboard and auto fill below'; pasteRow.append(pasteInput, pasteBtn); // --------------------------------------------------------------------- // Row 2: 搜索字段与操作行 (总宽度 420px) // --------------------------------------------------------------------- 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: 底部生成按钮行 (总宽度 420px,和上方完全平齐) // --------------------------------------------------------------------- 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'; // 解析与自动填充核心逻辑 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 + '\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(); })();