// ==UserScript== // @name GGn Video Games by Genre - Search Helper // @namespace feng9148scripts // @version 1.0 // @author feng9148 Original 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.game']], [/\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']], // ---- 以下根据 tags.txt 新增 ---- [/\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', 'Arcade', 'Auto Battler', 'Beat em Up', 'Board Game', 'Bullet Hell', 'Business Simulation', 'Card Battler', 'Card Game', 'City Building', 'Construction Sim', 'Creature Collector', 'Dating Simulation', 'Deck Building', 'Dungeon Crawler', 'Fighting', 'FMV', 'God Game', 'Grand Strategy', 'Graphic Adventure', 'Hack and Slash', 'Hidden Object', 'Horror', 'Idle', 'Interactive Movie', 'Life Simulation', 'Management', 'Match 3', 'Metroidvania', 'Party', 'Pinball', 'Platformer', 'Puzzle', 'Racing', 'Rail Shooter', 'Real-Time Strategy', 'Rhythm', 'Roguelike', 'Roguelite', 'RPG', 'Shoot em Up', 'Shooter', 'Simulation', 'Souls-like', 'Sports', 'Stealth', 'Strategy', 'Survival', 'Tactics', 'Tower Defense', 'Trivia', 'Turn-Based Strategy', 'Twin Stick', 'Tycoon', 'Visual Novel', 'Wargame', 'Walking Sim', '4X', ]; ///////// 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() { const now = new Date().getFullYear(); // Always a range of 4–8 years const lengths = [4, 4, 5, 5, 6, 6, 7, 7, 8]; const len = lengths[Math.floor(Math.random() * lengths.length)]; const minStart = 1990; const maxStart = now - len; const start = minStart + Math.floor(Math.random() * (maxStart - minStart + 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 combo = formatCombo(pickGenre(getRecentGenres(15)), ...Object.values(pickYearRange())); 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(); })();