// ==UserScript==
// @name CreeperWorld2 Maps Sorter
// @namespace http://tampermonkey.net/
// @version 1.2
// @description Sort all CW2 custom maps by downloads, scores, or comments across all pages
// @match https://knucklecracker.com/creeperworld2/viewmaps.php*
// @grant none
// @run-at document-end
// ==/UserScript==
(function () {
'use strict';
// ── Helpers ────────────────────────────────────────────────────────────────
function parseMapsFromDoc(doc) {
const maps = [];
const mapIdLinks = [...doc.querySelectorAll('a[href*="mapid="]')]
.filter(a => a.textContent.includes('Map ID:'));
for (const link of mapIdLinks) {
const mapId = link.href.match(/mapid=(\d+)/)?.[1];
const td = link.closest('td');
const nextTd = td?.nextElementSibling;
if (!nextTd) continue;
const getField = (label) => {
const labelEl = [...nextTd.querySelectorAll('td')]
.find(el => el.textContent.trim() === label);
return labelEl?.nextElementSibling?.textContent?.trim() ?? '';
};
const scoresText = getField('Scores:');
const scoresNum = parseInt(scoresText.split(/\s/)[0]) || 0;
const downloadsNum = parseInt(getField('Downloads:')) || 0;
const commentsNum = parseInt(getField('Comments:')) || 0;
const bestTime = getField('Best Time:');
const title = getField('Title:');
const author = getField('Author:');
if (!title) continue;
maps.push({ mapId, title, author, downloads: downloadsNum, scores: scoresNum, comments: commentsNum, bestTime });
}
return maps;
}
async function fetchPage(pageNum, author, duration) {
const url = `https://knucklecracker.com/creeperworld2/viewmaps.php?embedded=false&gameVer=&page=${pageNum}&duration=${duration}&author=${encodeURIComponent(author)}&search=`;
const res = await fetch(url);
const html = await res.text();
const parser = new DOMParser();
return parser.parseFromString(html, 'text/html');
}
function getTotalPages(doc) {
const text = doc.body.innerText.match(/(\d+) Custom Maps/);
const total = parseInt(text?.[1]) || 0;
return Math.ceil(total / 24);
}
// Read current filter params from URL
function getCurrentParams() {
const params = new URLSearchParams(window.location.search);
return {
author: params.get('author') || '',
duration: params.get('duration') || '0',
};
}
// ── UI ────────────────────────────────────────────────────────────────────
const TOOLBAR_ID = 'cw2-sorter-toolbar';
const OVERLAY_ID = 'cw2-sorter-overlay';
function buildToolbar() {
if (document.getElementById(TOOLBAR_ID)) return;
const bar = document.createElement('div');
bar.id = TOOLBAR_ID;
bar.style.cssText = `
background: #1a2535; border: 1px solid #4a6080; color: #e0e0e0;
padding: 8px 12px; margin: 8px auto; width: 738px; box-sizing: border-box;
font-family: sans-serif; font-size: 13px; display: flex; align-items: center; gap: 10px;
border-radius: 4px;
`;
bar.innerHTML = `
Sort All Maps:
`;
bar.querySelectorAll('button').forEach(btn => {
btn.addEventListener('click', () => startSort(btn.dataset.sort));
});
// Try multiple insertion strategies, most specific first
const inserted = (() => {
// 1. First map card's ancestor center element
const firstMapLink = document.querySelector('a[href*="mapid="]');
if (firstMapLink) {
const centerEl = firstMapLink.closest('center') || firstMapLink.closest('div[style*="background-color:#253040"]');
if (centerEl) {
centerEl.parentElement.insertBefore(bar, centerEl);
return true;
}
}
// 2. Any div containing "2980 Custom Maps" or similar count text
const countDiv = [...document.querySelectorAll('div')]
.find(d => /\d+ Custom Maps/.test(d.textContent) && d.children.length < 5);
if (countDiv) {
countDiv.parentElement.insertBefore(bar, countDiv);
return true;
}
// 3. Fallback: after the nav bar
const nav = document.querySelector('div[style*="titlebackgroundbarwide"]') ||
document.querySelector('center');
if (nav) {
nav.parentElement.insertBefore(bar, nav.nextSibling);
return true;
}
return false;
})();
if (!inserted) document.body.prepend(bar);
}
function btnStyle() {
return `background:#2a4060; color:#d0e8ff; border:1px solid #4a6080; border-radius:3px;
padding:4px 10px; cursor:pointer; font-size:12px;`;
}
function setStatus(msg) {
const el = document.getElementById('cw2-sorter-status');
if (el) el.textContent = msg;
}
function showOverlay(maps, sortField) {
// Remove previous overlay
document.getElementById(OVERLAY_ID)?.remove();
const overlay = document.createElement('div');
overlay.id = OVERLAY_ID;
overlay.style.cssText = `
width: 750px; margin: 10px auto; background: #1a2535;
border: 1px solid #4a6080; border-radius: 4px;
font-family: sans-serif; font-size: 12px; color: #e0e0e0;
`;
const header = document.createElement('div');
header.style.cssText = 'padding:8px 12px; background:#253040; display:flex; justify-content:space-between; align-items:center;';
header.innerHTML = `
${maps.length} Maps — sorted by ${sortField} (highest first)
`;
overlay.appendChild(header);
const table = document.createElement('table');
table.style.cssText = 'width:100%; border-collapse:collapse;';
table.innerHTML = `
#
Title
Author
Downloads ↕
Scores ↕
Comments ↕
Best Time