// ==UserScript==
// @name FlatMMO Data Pages Beautifier
// @namespace http://tampermonkey.net/
// @version 1.0
// @description Beautifies FlatMMO mining and woodcutting data pages with complete tool and resource level requirements
// @author Pizza1337
// @match https://flatmmo.com/data/mining.html
// @match https://flatmmo.com/data/woodcutting.html
// @grant GM_addStyle
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_xmlhttpRequest
// @license MIT
// ==/UserScript==
(function() {
'use strict';
// Get current page type
const pageType = window.location.pathname.includes('mining') ? 'mining' : 'woodcutting';
const toolType = pageType === 'mining' ? 'pickaxe' : 'axe';
const otherPage = pageType === 'mining' ? 'woodcutting' : 'mining';
const otherPageUrl = pageType === 'mining' ?
'https://flatmmo.com/data/woodcutting.html' :
'https://flatmmo.com/data/mining.html';
// Define available tools
const tools = ['bronze', 'iron', 'silver', 'gold', 'promethium', 'titanium', 'ancient'];
// Define level brackets
const levelBrackets = [
{min: 1, max: 9, label: '1-9'},
{min: 10, max: 19, label: '10-19'},
{min: 20, max: 29, label: '20-29'},
{min: 30, max: 39, label: '30-39'},
{min: 40, max: 49, label: '40-49'},
{min: 50, max: 59, label: '50-59'},
{min: 60, max: 69, label: '60-69'},
{min: 70, max: 79, label: '70-79'},
{min: 80, max: 89, label: '80-89'},
{min: 90, max: 99, label: '90-99'},
{min: 100, max: 100, label: '100'}
];
// Define tool level requirements
const toolLevelRequirements = {
mining: {
'bronze_pickaxe': 1,
'iron_pickaxe': 10,
'silver_pickaxe': 20,
'gold_pickaxe': 30,
'promethium_pickaxe': 50,
'titanium_pickaxe': 65,
'ancient_pickaxe': 101
},
woodcutting: {
'bronze_axe': 1,
'iron_axe': 10,
'silver_axe': 20,
'gold_axe': 30,
'promethium_axe': 50,
'titanium_axe': 65,
'ancient_axe': 101
}
};
const toolReqs = toolLevelRequirements[pageType];
// Define level requirements for resources
const resourceLevelRequirements = {
mining: {
'coal': 1,
'copper': 1,
'iron': 5,
'silver': 15,
'gold': 30,
'promethium': 50,
'titanium': 65,
'giant_coal': 1,
'giant_copper': 1,
'giant_iron': 5
},
woodcutting: {
'tree': 1,
'oak_tree': 10,
'willow_tree': 20,
'maple_tree': 30,
'mangrove_tree': 50,
'haunted_tree': 65
}
};
const levelReqs = resourceLevelRequirements[pageType];
// Parse the original table data BEFORE clearing the page
function parseTableData() {
const data = [];
// Try to find an actual table element
const tables = document.getElementsByTagName('table');
if (tables.length > 0) {
const table = tables[0];
const rows = table.getElementsByTagName('tr');
for (let i = 1; i < rows.length; i++) { // Skip header row
const cells = rows[i].getElementsByTagName('td');
if (cells.length >= 7) {
const resourceName = cells[0].textContent.trim();
const toolName = cells[2].textContent.trim();
const xpPerHourText = cells[6].textContent.trim();
const xpMatch = xpPerHourText.match(/([\d,]+\.?\d*)/);
const xpPerHourValue = xpMatch ? parseFloat(xpMatch[1].replace(/,/g, '')) : 0;
// Get base XP value
let xpValue = parseInt(cells[1].textContent.trim()) || 0;
// Fix giant resources XP (they give same as non-giant)
if (resourceName === 'giant_coal') xpValue = 10;
if (resourceName === 'giant_copper') xpValue = 15;
if (resourceName === 'giant_iron') xpValue = 40;
// Fix ticks (add 1 to all values)
const originalTicks = parseInt(cells[4].textContent.trim()) || 0;
const actualTicks = originalTicks + 1;
// Recalculate XP per tick and XP per hour with corrected values
const xpPerTick = xpValue / actualTicks;
const ticksPerHour = 3600 / (actualTicks * 0.5); // 0.5 seconds per tick
const actualXpPerHour = xpValue * ticksPerHour;
data.push({
resource: resourceName,
xp: xpValue,
tool: toolName,
toolLevelRequired: toolReqs[toolName] || 1,
level: parseInt(cells[3].textContent.trim()) || 0,
ticks: actualTicks,
xpPerTick: xpPerTick,
xpPerHour: actualXpPerHour,
xpPerHourText: `${actualXpPerHour.toLocaleString()} xp/h`,
levelRequired: levelReqs[resourceName] || 1
});
}
}
}
return data;
}
// Store the data BEFORE modifying the page
const tableData = parseTableData();
if (tableData.length === 0) {
alert('Error: Could not parse data from the page. Please refresh and try again.');
return;
}
// Store original order of resources
const resourceOrder = {};
const uniqueResources = [...new Set(tableData.map(item => item.resource))];
uniqueResources.forEach((resource, index) => {
resourceOrder[resource] = index;
});
// Fetch user level from hiscores API
async function fetchUserLevel(username) {
return new Promise((resolve) => {
const apiUrl = `https://flatmmo.com/api/hiscores/${pageType}.php`;
//console.log(`Fetching ${pageType} level for "${username}" from API: ${apiUrl}`);
GM_xmlhttpRequest({
method: 'GET',
url: apiUrl,
headers: {
'Accept': 'application/json'
},
onload: function(response) {
try {
// Parse JSON response
const data = JSON.parse(response.responseText);
//console.log(`API returned ${data.length} entries`);
// Convert username to lowercase for matching (API uses lowercase)
const searchUsername = username.toLowerCase().trim();
//console.log(`Searching for username: "${searchUsername}"`);
// Search for the username in the data
for (let i = 0; i < data.length; i++) {
const entry = data[i];
const entryUsername = entry.username ? entry.username.toLowerCase().trim() : '';
if (entryUsername === searchUsername) {
// Get the level based on page type
const levelField = `${pageType}_level`;
const xpField = `${pageType}_xp`;
//console.log(`Found matching entry for "${searchUsername}":`, entry);
//console.log(`Looking for level field: "${levelField}"`);
//console.log(`Level field value: ${entry[levelField]}`);
const level = parseInt(entry[levelField]);
const xp = entry[xpField] || 0;
if (!isNaN(level) && level > 0) {
//console.log(`✅ Found ${username} with ${pageType} level: ${level} (XP: ${xp.toLocaleString()})`);
resolve(level);
return;
} else {
console.error(`❌ Invalid level value: ${entry[levelField]}`);
console.error(`Full entry data:`, entry);
console.error(`All fields in entry:`, Object.keys(entry));
}
}
}
console.log(`❌ Username "${searchUsername}" not found in API data`);
// Check if username might be there with different casing or spaces
const possibleMatch = data.find(entry =>
entry.username && entry.username.replace(/\s/g, '').toLowerCase() === searchUsername.replace(/\s/g, '')
);
if (possibleMatch) {
console.log(`Found possible match with different formatting: "${possibleMatch.username}"`);
}
// Log first few entries for debugging
if (data.length > 0) {
//console.log('First 5 entries from API:');
data.slice(0, 5).forEach(entry => {
//console.log(`- "${entry.username}": Level ${entry[`${pageType}_level`]}`);
});
}
resolve(null);
} catch (error) {
console.error('Error parsing API response:', error);
console.log('Response text:', response.responseText.substring(0, 500));
resolve(null);
}
},
onerror: function(error) {
console.error('Failed to fetch from API:', error);
resolve(null);
}
});
});
}
// Inject modern styles
GM_addStyle(`
@import url('https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;500;600;700&display=swap');
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Poppins', system-ui, -apple-system, sans-serif;
background: #0a0e27;
color: #e8e6e3;
padding: 0;
min-height: 100vh;
overflow-x: hidden;
}
body::before {
content: '';
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background:
radial-gradient(circle at 20% 50%, rgba(120, 80, 255, 0.15) 0%, transparent 50%),
radial-gradient(circle at 80% 80%, rgba(0, 255, 136, 0.1) 0%, transparent 50%),
radial-gradient(circle at 40% 20%, rgba(0, 212, 255, 0.1) 0%, transparent 50%);
pointer-events: none;
z-index: 1;
}
.container {
max-width: 1600px;
margin: 0 auto;
padding: 20px;
position: relative;
z-index: 2;
}
.header {
text-align: center;
padding: 60px 20px 40px;
position: relative;
}
.header h1 {
font-size: 4em;
font-weight: 700;
margin: 0;
background: linear-gradient(135deg, #667eea 0%, #00ff88 50%, #00d4ff 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
text-transform: uppercase;
letter-spacing: 3px;
animation: glow 3s ease-in-out infinite;
}
@keyframes glow {
0%, 100% { filter: brightness(1); }
50% { filter: brightness(1.2); }
}
.header p {
margin-top: 10px;
color: #8892b0;
font-size: 1.2em;
}
.page-switcher {
position: absolute;
top: 20px;
right: 20px;
z-index: 10;
}
.page-switcher a {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 12px 24px;
background: linear-gradient(135deg, rgba(102, 126, 234, 0.2), rgba(0, 255, 136, 0.2));
border: 1px solid rgba(0, 255, 136, 0.3);
border-radius: 12px;
color: #00ff88;
text-decoration: none;
font-weight: 500;
transition: all 0.3s;
}
.page-switcher a:hover {
transform: translateY(-2px);
box-shadow: 0 10px 30px rgba(0, 255, 136, 0.3);
background: linear-gradient(135deg, rgba(102, 126, 234, 0.3), rgba(0, 255, 136, 0.3));
}
.user-section {
background: rgba(255, 255, 255, 0.03);
backdrop-filter: blur(20px);
border-radius: 25px;
padding: 25px;
margin-bottom: 30px;
border: 1px solid rgba(255, 255, 255, 0.08);
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
}
.user-input-container {
display: flex;
gap: 15px;
align-items: center;
justify-content: center;
flex-wrap: wrap;
}
.user-input-container label {
color: #00ff88;
font-weight: 500;
}
.user-input-container input {
padding: 10px 15px;
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 12px;
color: white;
font-family: inherit;
width: 200px;
}
.user-input-container button {
padding: 10px 20px;
background: linear-gradient(135deg, #667eea, #00ff88);
border: none;
border-radius: 12px;
color: white;
font-weight: 600;
cursor: pointer;
transition: all 0.3s;
}
.user-input-container button:hover {
transform: translateY(-2px);
box-shadow: 0 10px 30px rgba(0, 255, 136, 0.3);
}
.user-level-display {
margin-top: 15px;
text-align: center;
padding: 15px;
background: rgba(0, 255, 136, 0.1);
border-radius: 12px;
border: 1px solid rgba(0, 255, 136, 0.2);
}
.user-level-display .level-text {
font-size: 1.2em;
color: #00ff88;
font-weight: 600;
}
.tool-selector {
background: rgba(255, 255, 255, 0.03);
backdrop-filter: blur(20px);
border-radius: 25px;
padding: 30px;
margin-bottom: 40px;
border: 1px solid rgba(255, 255, 255, 0.08);
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
}
.tool-selector h2 {
margin: 0 0 25px 0;
background: linear-gradient(90deg, #00ff88, #00d4ff);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
font-size: 1.5em;
font-weight: 600;
text-align: center;
}
.tool-grid {
display: flex;
justify-content: center;
flex-wrap: wrap;
gap: 15px;
margin-bottom: 25px;
}
.tool-option {
display: flex;
flex-direction: column;
align-items: center;
padding: 20px;
background: rgba(255, 255, 255, 0.05);
border: 2px solid transparent;
border-radius: 20px;
cursor: pointer;
transition: all 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275);
position: relative;
overflow: hidden;
min-width: 100px;
}
.tool-option::before {
content: '';
position: absolute;
top: 50%;
left: 50%;
width: 100%;
height: 100%;
background: radial-gradient(circle, rgba(255, 255, 255, 0.1) 0%, transparent 70%);
transform: translate(-50%, -50%) scale(0);
transition: transform 0.5s;
}
.tool-option:hover::before {
transform: translate(-50%, -50%) scale(2);
}
.tool-option:hover {
transform: translateY(-5px) scale(1.05);
box-shadow: 0 15px 40px rgba(0, 255, 136, 0.3);
}
.tool-option.active {
background: linear-gradient(135deg, rgba(102, 126, 234, 0.2), rgba(0, 255, 136, 0.2));
border-color: #00ff88;
box-shadow: 0 0 30px rgba(0, 255, 136, 0.4);
transform: scale(1.05);
}
.tool-option img {
width: 56px;
height: 56px;
margin-bottom: 10px;
filter: drop-shadow(0 4px 12px rgba(0, 0, 0, 0.4));
position: relative;
z-index: 1;
}
.tool-option span {
font-size: 0.95em;
font-weight: 500;
text-transform: capitalize;
position: relative;
z-index: 1;
}
.filters {
display: flex;
gap: 20px;
align-items: center;
justify-content: center;
flex-wrap: wrap;
}
.filter-group {
display: flex;
align-items: center;
gap: 10px;
background: rgba(255, 255, 255, 0.05);
padding: 10px 20px;
border-radius: 15px;
}
.filter-group label {
color: #00ff88;
font-weight: 500;
font-size: 0.9em;
}
.filter-group select {
background: rgba(255, 255, 255, 0.1);
border: 1px solid rgba(255, 255, 255, 0.2);
color: white;
padding: 8px 12px;
border-radius: 10px;
font-family: inherit;
cursor: pointer;
transition: all 0.3s;
}
.filter-group select:hover {
background: rgba(255, 255, 255, 0.15);
border-color: rgba(0, 255, 136, 0.5);
}
.filter-group select:focus {
outline: none;
border-color: #00ff88;
box-shadow: 0 0 10px rgba(0, 255, 136, 0.3);
}
.filter-group select option {
background: #1a1f3a;
color: white;
}
.level-brackets {
display: flex;
gap: 10px;
flex-wrap: wrap;
justify-content: center;
}
.bracket-btn {
padding: 8px 16px;
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 10px;
color: #8892b0;
cursor: pointer;
transition: all 0.3s;
font-weight: 500;
font-size: 0.9em;
}
.bracket-btn:hover {
background: rgba(255, 255, 255, 0.1);
color: white;
transform: translateY(-2px);
}
.bracket-btn.active {
background: linear-gradient(135deg, #667eea, #00ff88);
border-color: transparent;
color: white;
}
.bracket-btn.auto-selected {
background: linear-gradient(135deg, #00ff88, #00d4ff);
border-color: transparent;
color: white;
box-shadow: 0 0 20px rgba(0, 255, 136, 0.4);
}
.view-toggle {
display: flex;
gap: 10px;
justify-content: center;
margin: 30px 0;
}
.view-btn {
padding: 12px 24px;
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 12px;
color: white;
cursor: pointer;
transition: all 0.3s;
font-weight: 500;
}
.view-btn.active {
background: linear-gradient(135deg, #667eea, #00ff88);
border-color: transparent;
}
.view-btn:hover:not(.active) {
background: rgba(255, 255, 255, 0.1);
}
.stats-header {
display: flex;
justify-content: center;
margin: 40px 0;
}
.stat-card {
background: linear-gradient(135deg, rgba(102, 126, 234, 0.2), rgba(0, 255, 136, 0.2));
backdrop-filter: blur(10px);
border-radius: 25px;
padding: 30px 40px;
text-align: center;
border: 2px solid rgba(0, 255, 136, 0.3);
transition: all 0.3s;
position: relative;
overflow: hidden;
min-width: 400px;
box-shadow: 0 20px 60px rgba(0, 255, 136, 0.2);
}
.stat-card::before {
content: '';
position: absolute;
top: -50%;
left: -50%;
width: 200%;
height: 200%;
background: linear-gradient(45deg, transparent, rgba(255, 255, 255, 0.1), transparent);
animation: shimmer 3s ease-in-out infinite;
}
@keyframes shimmer {
0% { transform: translateX(-100%) translateY(-100%) rotate(45deg); }
50% { transform: translateX(0%) translateY(0%) rotate(45deg); }
100% { transform: translateX(100%) translateY(100%) rotate(45deg); }
}
.stat-card:hover {
transform: translateY(-5px) scale(1.02);
box-shadow: 0 25px 70px rgba(102, 126, 234, 0.3);
}
.stat-value {
font-size: 3em;
font-weight: 700;
background: linear-gradient(135deg, #ffd700, #00ff88, #00d4ff);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
margin-bottom: 10px;
text-shadow: 0 0 30px rgba(0, 255, 136, 0.5);
}
.stat-label {
color: #00ff88;
font-size: 1.2em;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 2px;
margin-bottom: 15px;
}
.stat-sublabel {
color: #e8e6e3;
font-size: 1.1em;
margin-top: 5px;
font-weight: 500;
}
.stat-resource {
color: #00d4ff;
font-size: 1.3em;
font-weight: 600;
text-transform: capitalize;
}
.stat-level {
color: #ffd700;
font-weight: 700;
}
.resources-container {
margin-top: 40px;
}
/* Card View Styles */
.resource-cards {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
gap: 25px;
animation: fadeIn 0.5s ease-in;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(20px); }
to { opacity: 1; transform: translateY(0); }
}
.resource-card {
background: rgba(255, 255, 255, 0.03);
backdrop-filter: blur(10px);
border-radius: 20px;
border: 1px solid rgba(255, 255, 255, 0.08);
overflow: hidden;
transition: all 0.3s;
cursor: pointer;
}
.resource-card:hover {
transform: translateY(-5px) scale(1.02);
box-shadow: 0 20px 50px rgba(0, 255, 136, 0.2);
border-color: rgba(0, 255, 136, 0.3);
}
.resource-header {
padding: 20px;
background: linear-gradient(135deg, rgba(102, 126, 234, 0.2), rgba(0, 255, 136, 0.1));
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
display: flex;
justify-content: space-between;
align-items: center;
}
.resource-name {
font-size: 1.3em;
font-weight: 600;
color: #00d4ff;
text-transform: capitalize;
display: flex;
align-items: center;
gap: 12px;
}
.resource-icon {
width: 40px;
height: 40px;
filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.3));
object-fit: contain;
}
.resource-levels {
padding: 20px;
max-height: 300px;
overflow-y: auto;
}
.resource-levels::-webkit-scrollbar {
width: 6px;
}
.resource-levels::-webkit-scrollbar-track {
background: rgba(255, 255, 255, 0.05);
border-radius: 3px;
}
.resource-levels::-webkit-scrollbar-thumb {
background: linear-gradient(135deg, #667eea, #00ff88);
border-radius: 3px;
}
.level-entry {
display: grid;
grid-template-columns: auto 1fr auto;
gap: 15px;
padding: 12px;
margin-bottom: 10px;
background: rgba(255, 255, 255, 0.02);
border-radius: 12px;
align-items: center;
transition: all 0.3s;
}
.level-entry:hover {
background: rgba(255, 255, 255, 0.05);
transform: translateX(5px);
}
.level-entry:last-child {
margin-bottom: 0;
}
.level-badge {
background: linear-gradient(135deg, #ff6b6b, #ff8e53);
color: white;
padding: 6px 12px;
border-radius: 20px;
font-weight: 600;
font-size: 0.85em;
}
.level-stats {
display: flex;
gap: 20px;
align-items: center;
}
.stat-item {
display: flex;
flex-direction: column;
align-items: center;
}
.stat-item-value {
font-weight: 600;
color: #00ff88;
font-size: 0.95em;
}
.stat-item-label {
font-size: 0.75em;
color: #8892b0;
text-transform: uppercase;
letter-spacing: 1px;
}
.xp-hour-badge {
background: linear-gradient(135deg, #667eea, #764ba2);
padding: 8px 16px;
border-radius: 12px;
font-weight: 600;
font-size: 0.9em;
white-space: nowrap;
}
/* List View Styles */
.resource-list {
background: rgba(255, 255, 255, 0.03);
backdrop-filter: blur(10px);
border-radius: 20px;
border: 1px solid rgba(255, 255, 255, 0.08);
overflow: hidden;
animation: fadeIn 0.5s ease-in;
}
.list-header {
display: grid;
grid-template-columns: 2fr 1fr 1fr 1fr 1fr 1.5fr;
padding: 20px;
background: linear-gradient(135deg, rgba(102, 126, 234, 0.2), rgba(0, 255, 136, 0.1));
border-bottom: 2px solid rgba(0, 255, 136, 0.3);
font-weight: 600;
color: #00ff88;
text-transform: uppercase;
font-size: 0.9em;
letter-spacing: 1px;
}
.list-item {
display: grid;
grid-template-columns: 2fr 1fr 1fr 1fr 1fr 1.5fr;
padding: 20px;
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
align-items: center;
transition: all 0.3s;
cursor: pointer;
}
.list-item:hover {
background: rgba(255, 255, 255, 0.05);
transform: translateX(10px);
}
.list-item:last-child {
border-bottom: none;
}
.list-resource {
display: flex;
align-items: center;
gap: 12px;
font-weight: 600;
color: #00d4ff;
text-transform: capitalize;
}
.list-level {
color: #ff6b6b;
font-weight: 600;
}
.list-ticks {
color: #00ff88;
}
.list-xp {
color: #ffd700;
font-weight: 500;
}
.sort-indicator {
display: inline-block;
margin-left: 5px;
transition: transform 0.3s;
}
.sort-asc::after {
content: '▲';
font-size: 0.8em;
}
.sort-desc::after {
content: '▼';
font-size: 0.8em;
}
.hidden {
display: none !important;
}
.empty-state {
text-align: center;
padding: 60px 20px;
color: #8892b0;
}
.empty-state h3 {
font-size: 1.5em;
margin-bottom: 10px;
color: #64748b;
}
`);
// Create new UI
document.body.innerHTML = '';
const container = document.createElement('div');
container.className = 'container';
// Page switcher
const pageSwitcher = document.createElement('div');
pageSwitcher.className = 'page-switcher';
pageSwitcher.innerHTML = `
<a href="${otherPageUrl}">
${otherPage === 'mining' ? '⛏️' : '🪓'} Switch to ${otherPage.charAt(0).toUpperCase() + otherPage.slice(1)}
</a>
`;
container.appendChild(pageSwitcher);
// Header
const header = document.createElement('div');
header.className = 'header';
header.innerHTML = `
<h1>${pageType}</h1>
<p>Optimize your XP gains with the perfect tool and level combination</p>
`;
container.appendChild(header);
// User section
const userSection = document.createElement('div');
userSection.className = 'user-section';
userSection.innerHTML = `
<div class="user-input-container">
<label>Username:</label>
<input type="text" id="usernameInput" placeholder="Enter your username">
<button id="fetchLevelBtn">Fetch Level</button>
<button id="clearUserBtn" style="background: linear-gradient(135deg, #ff6b6b, #ff8e53);">Clear</button>
</div>
<div id="userLevelDisplay" class="user-level-display hidden">
<div class="level-text">Loading...</div>
</div>
`;
container.appendChild(userSection);
// Tool selector
const toolSelector = document.createElement('div');
toolSelector.className = 'tool-selector';
toolSelector.innerHTML = `
<h2>⚒️ Select Your Tool</h2>
<div class="tool-grid" id="toolGrid"></div>
<div class="filters">
<div class="filter-group">
<label>Level Range:</label>
<div class="level-brackets" id="levelBrackets"></div>
</div>
<div class="filter-group">
<label>Sort by:</label>
<select id="sortBy">
<option value="resource">Resource (Original Order)</option>
<option value="xpPerHour">XP per Hour</option>
</select>
</div>
</div>
`;
container.appendChild(toolSelector);
const toolGrid = toolSelector.querySelector('#toolGrid');
const levelBracketsContainer = toolSelector.querySelector('#levelBrackets');
// Add tool options
tools.forEach(tool => {
const toolFullName = `${tool}_${toolType}`;
const toolOption = document.createElement('div');
toolOption.className = 'tool-option';
toolOption.dataset.tool = toolFullName;
const toolLevelReq = toolReqs[toolFullName] || 1;
toolOption.innerHTML = `
<img src="https://flatmmo.com/images/items/${toolFullName}.png"
alt="${tool} ${toolType}"
onerror="this.src='data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 width=%2256%22 height=%2256%22 viewBox=%220 0 56 56%22><rect width=%2256%22 height=%2256%22 fill=%22%23444%22 rx=%228%22/><text x=%2228%22 y=%2235%22 text-anchor=%22middle%22 fill=%22%23aaa%22 font-size=%2224%22>⚒️</text></svg>'">
<span>${tool}</span>
<span style="font-size: 0.75em; color: #ff6b6b;">Lv ${toolLevelReq}</span>
`;
toolOption.addEventListener('click', () => filterByTool(toolFullName));
toolGrid.appendChild(toolOption);
});
// Add "All Levels" button first
const allLevelsBtn = document.createElement('button');
allLevelsBtn.className = 'bracket-btn active';
allLevelsBtn.dataset.min = '1';
allLevelsBtn.dataset.max = '100';
allLevelsBtn.textContent = 'All Levels';
allLevelsBtn.addEventListener('click', () => selectLevelBracket({min: 1, max: 100, label: 'All Levels'}));
levelBracketsContainer.appendChild(allLevelsBtn);
// Add level brackets
levelBrackets.forEach(bracket => {
const bracketBtn = document.createElement('button');
bracketBtn.className = 'bracket-btn';
bracketBtn.dataset.min = bracket.min;
bracketBtn.dataset.max = bracket.max;
bracketBtn.textContent = bracket.label;
bracketBtn.addEventListener('click', () => selectLevelBracket(bracket));
levelBracketsContainer.appendChild(bracketBtn);
});
// View toggle
const viewToggle = document.createElement('div');
viewToggle.className = 'view-toggle';
viewToggle.innerHTML = `
<button class="view-btn active" data-view="cards">📊 Card View</button>
<button class="view-btn" data-view="list">📋 List View</button>
`;
container.appendChild(viewToggle);
// Stats summary
const statsHeader = document.createElement('div');
statsHeader.className = 'stats-header';
statsHeader.id = 'statsHeader';
container.appendChild(statsHeader);
// Resources container
const resourcesContainer = document.createElement('div');
resourcesContainer.className = 'resources-container';
resourcesContainer.id = 'resourcesContainer';
container.appendChild(resourcesContainer);
document.body.appendChild(container);
// State management
let currentTool = 'all';
let minLevelFilter = 1;
let maxLevelFilter = 100;
let currentView = 'cards';
let sortBy = 'resource';
let sortOrder = 'asc';
let currentUsername = GM_getValue('flatmmo_username', '');
let currentUserLevel = null;
// Define functions before using them
function selectLevelBracket(bracket) {
minLevelFilter = bracket.min;
maxLevelFilter = bracket.max;
// Update button states
document.querySelectorAll('.bracket-btn').forEach(btn => {
btn.classList.remove('active', 'auto-selected');
if (btn.dataset.min == bracket.min && btn.dataset.max == bracket.max) {
btn.classList.add('active');
}
});
applyFilters();
}
function filterByTool(tool) {
// Update active states
document.querySelectorAll('.tool-option').forEach(opt => {
opt.classList.toggle('active', opt.dataset.tool === tool);
});
// If clicking the same tool, toggle to show all
if (currentTool === tool) {
currentTool = 'all';
document.querySelectorAll('.tool-option').forEach(opt => {
opt.classList.remove('active');
});
} else {
currentTool = tool;
}
applyFilters();
}
// Load saved username
if (currentUsername) {
document.getElementById('usernameInput').value = currentUsername;
fetchAndSetUserLevel(currentUsername);
}
// Function definitions
function selectLevelBracket(bracket) {
minLevelFilter = bracket.min;
maxLevelFilter = bracket.max;
// Update button states
document.querySelectorAll('.bracket-btn').forEach(btn => {
btn.classList.remove('active', 'auto-selected');
if (btn.dataset.min == bracket.min && btn.dataset.max == bracket.max) {
btn.classList.add('active');
}
});
applyFilters();
}
function findBestBracketForLevel(level) {
// Find the bracket that contains the user's level
for (let bracket of levelBrackets) {
if (level >= bracket.min && level <= bracket.max) {
return bracket;
}
}
return {min: 1, max: 100, label: 'All Levels'};
}
async function fetchAndSetUserLevel(username) {
const levelDisplay = document.getElementById('userLevelDisplay');
const levelText = levelDisplay.querySelector('.level-text');
levelDisplay.classList.remove('hidden');
levelText.textContent = 'Fetching level...';
//console.log(`Starting level fetch for username: "${username}"`);
const level = await fetchUserLevel(username);
//console.log(`fetchUserLevel returned: ${level}`);
if (level !== null && level > 0) {
currentUserLevel = level;
levelText.innerHTML = `
<strong style="color: #00d4ff;">${username}</strong>
<span style="color: #8892b0;">•</span>
${pageType.charAt(0).toUpperCase() + pageType.slice(1)} Level:
<strong style="color: #ffd700;">${level}</strong>
`;
// Auto-select the appropriate level bracket
const bestBracket = findBestBracketForLevel(level);
minLevelFilter = bestBracket.min;
maxLevelFilter = bestBracket.max;
// Update button states
document.querySelectorAll('.bracket-btn').forEach(btn => {
btn.classList.remove('active', 'auto-selected');
if (btn.dataset.min == bestBracket.min && btn.dataset.max == bestBracket.max) {
btn.classList.add('auto-selected');
}
});
applyFilters();
} else {
levelText.innerHTML = `
<span style="color: #ff6b6b;">⚠️ Username not found in hiscores</span>
<br>
<span style="color: #8892b0; font-size: 0.9em;">
Make sure the username exists in the ${pageType} hiscores (may need to be in top players)
</span>
`;
currentUserLevel = null;
}
}
function updateStats(data) {
const statsHeader = document.getElementById('statsHeader');
if (data.length === 0) {
statsHeader.innerHTML = `
<div class="stat-card">
<div class="stat-label">⚠️ No Data Available</div>
<div class="stat-sublabel">Try adjusting your filters or check level requirements</div>
</div>
`;
return;
}
const bestXp = Math.max(...data.map(d => d.xpPerHour));
const bestItem = data.find(d => d.xpPerHour === bestXp);
statsHeader.innerHTML = `
<div class="stat-card">
<div class="stat-label">🏆 Best XP Per Hour</div>
<div class="stat-value">${Math.round(bestXp).toLocaleString()}</div>
<div class="stat-sublabel">
<span class="stat-resource">${bestItem.resource.replace(/_/g, ' ')}</span>
at
<span class="stat-level">Level ${bestItem.level}</span>
</div>
<div class="stat-sublabel" style="margin-top: 5px;">
<span style="color: #ff6b6b;">Resource requires Lv ${bestItem.levelRequired} • Tool requires Lv ${bestItem.toolLevelRequired}</span>
</div>
<div class="stat-sublabel" style="margin-top: 10px; opacity: 0.8;">
${bestItem.tool.replace(/_/g, ' ')} • ${bestItem.ticks} ticks • ${bestItem.xp} XP per resource
</div>
</div>
`;
}
function getResourceImage(resource) {
if (pageType === 'woodcutting') {
// Use wiki images for trees
const treeImages = {
'tree': 'https://flatmmo.wiki/images/7/76/Normal_tree.png',
'oak_tree': 'https://flatmmo.wiki/images/c/cf/Oak_tree.png',
'willow_tree': 'https://flatmmo.wiki/images/1/19/Willow_tree.png',
'maple_tree': 'https://flatmmo.wiki/images/4/4b/Maple_tree.png',
'mangrove_tree': 'https://flatmmo.wiki/images/6/67/Mangrove_tree.png',
'haunted_tree': 'https://flatmmo.wiki/images/a/a7/Haunted_tree.png'
};
return treeImages[resource] || `https://flatmmo.com/images/items/${resource}.png`;
} else if (pageType === 'mining') {
// Use wiki images for rocks
const rockImages = {
'giant_coal': 'https://flatmmo.wiki/images/f/f8/Coal_rock.png',
'giant_copper': 'https://flatmmo.wiki/images/8/89/Copper_rock.png',
'giant_iron': 'https://flatmmo.wiki/images/d/db/Iron_rock.png'
};
return rockImages[resource] || `https://flatmmo.com/images/items/${resource}.png`;
}
return `https://flatmmo.com/images/items/${resource}.png`;
}
function renderCardView(data) {
const container = document.getElementById('resourcesContainer');
if (data.length === 0) {
container.innerHTML = `
<div class="empty-state">
<h3>No resources found</h3>
<p>Try adjusting your filters or selecting a different tool</p>
<p style="color: #ff6b6b; margin-top: 10px;">
Note: Resources and tools you can't use at your level are hidden
</p>
</div>
`;
return;
}
// Group data by resource
const groupedData = {};
data.forEach(item => {
if (!groupedData[item.resource]) {
groupedData[item.resource] = [];
}
groupedData[item.resource].push(item);
});
// Sort each group by level
Object.keys(groupedData).forEach(resource => {
groupedData[resource].sort((a, b) => a.level - b.level);
});
// Sort resources
const sortedResources = Object.keys(groupedData).sort((a, b) => {
if (sortBy === 'xpPerHour') {
const maxA = Math.max(...groupedData[a].map(item => item.xpPerHour));
const maxB = Math.max(...groupedData[b].map(item => item.xpPerHour));
return sortOrder === 'asc' ? maxA - maxB : maxB - maxA;
} else {
// Use original order
return resourceOrder[a] - resourceOrder[b];
}
});
const cardsHtml = sortedResources.map(resource => {
const levels = groupedData[resource];
const bestLevel = levels.reduce((best, current) =>
current.xpPerHour > best.xpPerHour ? current : best
);
const levelReq = levels[0].levelRequired;
return `
<div class="resource-card">
<div class="resource-header">
<div class="resource-name">
<img src="${getResourceImage(resource)}"
class="resource-icon"
onerror="this.style.display='none'">
${resource.replace(/_/g, ' ')}
</div>
<div style="color: #ff6b6b; font-size: 0.9em;">Requires Lv ${levelReq}</div>
</div>
<div class="resource-levels">
${levels.map(level => `
<div class="level-entry ${level === bestLevel ? 'best-level' : ''}">
<div class="level-badge">Lv ${level.level}</div>
<div class="level-stats">
<div class="stat-item">
<span class="stat-item-value">${level.ticks}</span>
<span class="stat-item-label">Ticks</span>
</div>
<div class="stat-item">
<span class="stat-item-value">${level.xp}</span>
<span class="stat-item-label">XP</span>
</div>
</div>
<div class="xp-hour-badge">${Math.round(level.xpPerHour).toLocaleString()} xp/h</div>
</div>
`).join('')}
</div>
</div>
`;
}).join('');
container.innerHTML = `<div class="resource-cards">${cardsHtml}</div>`;
}
function renderListView(data) {
const container = document.getElementById('resourcesContainer');
if (data.length === 0) {
container.innerHTML = `
<div class="empty-state">
<h3>No resources found</h3>
<p>Try adjusting your filters or selecting a different tool</p>
<p style="color: #ff6b6b; margin-top: 10px;">
Note: Resources and tools you can't use at your level are hidden
</p>
</div>
`;
return;
}
// Sort data
const sortedData = [...data].sort((a, b) => {
if (sortBy === 'xpPerHour') {
return sortOrder === 'asc' ? a.xpPerHour - b.xpPerHour : b.xpPerHour - a.xpPerHour;
} else {
// Sort by resource using original order
const orderDiff = resourceOrder[a.resource] - resourceOrder[b.resource];
if (orderDiff !== 0) return orderDiff;
// If same resource, sort by level
return a.level - b.level;
}
});
const listHtml = `
<div class="resource-list">
<div class="list-header">
<div class="sortable" data-sort="resource">Resource <span class="sort-indicator ${sortBy === 'resource' ? `sort-${sortOrder}` : ''}"></span></div>
<div>Req. Level</div>
<div>Your Level</div>
<div>Ticks</div>
<div>XP</div>
<div class="sortable" data-sort="xpPerHour">XP/Hour <span class="sort-indicator ${sortBy === 'xpPerHour' ? `sort-${sortOrder}` : ''}"></span></div>
</div>
${sortedData.map(item => `
<div class="list-item">
<div class="list-resource">
<img src="${getResourceImage(item.resource)}"
class="resource-icon"
onerror="this.style.display='none'">
${item.resource.replace(/_/g, ' ')}
</div>
<div class="list-level" style="color: #ff6b6b;">${item.levelRequired}</div>
<div class="list-level">${item.level}</div>
<div class="list-ticks">${item.ticks}</div>
<div class="list-xp">${item.xp}</div>
<div class="xp-hour-badge">${Math.round(item.xpPerHour).toLocaleString()} /h</div>
</div>
`).join('')}
</div>
`;
container.innerHTML = listHtml;
// Add click handlers for sorting
container.querySelectorAll('.sortable').forEach(header => {
header.style.cursor = 'pointer';
header.addEventListener('click', () => {
const newSortBy = header.dataset.sort;
if (sortBy === newSortBy) {
sortOrder = sortOrder === 'asc' ? 'desc' : 'asc';
} else {
sortBy = newSortBy;
sortOrder = sortBy === 'xpPerHour' ? 'desc' : 'asc'; // Default desc for XP/hour
}
document.getElementById('sortBy').value = sortBy;
applyFilters();
});
});
}
function applyFilters() {
let filteredData = tableData;
// Filter by tool
if (currentTool !== 'all') {
filteredData = filteredData.filter(row => row.tool === currentTool);
}
// Filter by level - check both resource and tool requirements
filteredData = filteredData.filter(row => {
// Check if player level is within the bracket
const inBracket = row.level >= minLevelFilter && row.level <= maxLevelFilter;
// Check if player can actually harvest this resource
const canHarvestResource = row.level >= row.levelRequired;
// Check if player can use this tool
const canUseTool = row.level >= row.toolLevelRequired;
return inBracket && canHarvestResource && canUseTool;
});
// Update stats
updateStats(filteredData);
// Render view
if (currentView === 'cards') {
renderCardView(filteredData);
} else {
renderListView(filteredData);
}
}
function filterByTool(tool) {
// Update active states
document.querySelectorAll('.tool-option').forEach(opt => {
opt.classList.toggle('active', opt.dataset.tool === tool);
});
// If clicking the same tool, toggle to show all
if (currentTool === tool) {
currentTool = 'all';
document.querySelectorAll('.tool-option').forEach(opt => {
opt.classList.remove('active');
});
} else {
currentTool = tool;
}
applyFilters();
}
// Event listeners
document.getElementById('fetchLevelBtn').addEventListener('click', async () => {
const username = document.getElementById('usernameInput').value.trim();
if (username) {
//console.log(`Fetching level for username: "${username}"`);
GM_setValue('flatmmo_username', username);
currentUsername = username;
await fetchAndSetUserLevel(username);
} else {
alert('Please enter a username');
}
});
document.getElementById('clearUserBtn').addEventListener('click', () => {
GM_setValue('flatmmo_username', '');
currentUsername = '';
currentUserLevel = null;
document.getElementById('usernameInput').value = '';
document.getElementById('userLevelDisplay').classList.add('hidden');
// Reset to all levels
selectLevelBracket({min: 1, max: 100, label: 'All Levels'});
});
document.getElementById('usernameInput').addEventListener('keypress', async (e) => {
if (e.key === 'Enter') {
const username = e.target.value.trim();
if (username) {
GM_setValue('flatmmo_username', username);
currentUsername = username;
await fetchAndSetUserLevel(username);
}
}
});
document.getElementById('sortBy').addEventListener('change', (e) => {
sortBy = e.target.value;
sortOrder = sortBy === 'xpPerHour' ? 'desc' : 'asc';
applyFilters();
});
// View toggle
document.querySelectorAll('.view-btn').forEach(btn => {
btn.addEventListener('click', () => {
document.querySelectorAll('.view-btn').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
currentView = btn.dataset.view;
applyFilters();
});
});
// Initialize and load saved username
applyFilters();
// Load saved username after all functions are defined
if (currentUsername) {
document.getElementById('usernameInput').value = currentUsername;
fetchAndSetUserLevel(currentUsername);
}
})();