// ==UserScript== // @name GGn Video Games by Genre - Search Helper // @namespace waiter7scripts // @version 1.2.1 // @author waiter7 // @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']], ]; 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(); // 当前年份 (2026) const lengths = [4, 4, 5, 5, 6, 6, 7, 7, 8]; const len = lengths[Math.floor(Math.random() * lengths.length)]; // 1. 设置最低年限(普通游戏1990,塔防2007) let minStart = 1990; if (genre && genre.toLowerCase() === 'tower defense') { minStart = 2007; } // 2. 动态锁死上限,确保 结束年(start + len - 1) 最大不能超过 2026 const maxStart = now - len + 1; // 3. 安全兜底 const safeMinStart = minStart > maxStart ? maxStart : minStart; // 4. 摇出最终的起始年份 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;'; // 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;'; const lp = getLastPost(); if (lp && lp.text) { const parsed = parsePrompt(lp.text); if (parsed) { genreInput.value = parsed.genre; yearFrom.value = parsed.yearStart; yearTo.value = parsed.yearEnd != null ? parsed.yearEnd : ''; } } 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)); // 1. 先抽取类型 const years = pickYearRange(selectedGenre); // 2. 根据类型限定范围抽取年份 const combo = formatCombo(selectedGenre, years.start, years.end); // 3. 完美结合 textarea.value = textarea.value + '\n\n' + combo; textarea.focus(); textarea.setSelectionRange(textarea.value.length, textarea.value.length); }); genRow.appendChild(genBtn); panel.append(title, searchRow, genRow); afterNode.after(panel); } inject(); })();