// ==UserScript==
// @name FlatMMO Update Log Beautifier
// @namespace http://tampermonkey.net/
// @version 1.0
// @description Makes FlatMMO update log beautiful and organized by month with clean formatting
// @author Pizza1337
// @match *://flatmmo.com/updatelog*
// @icon https://flatmmo.com/favicon.ico
// @grant none
// @license MIT
// ==/UserScript==
(function() {
'use strict';
// Wait for the page to load
window.addEventListener('load', function() {
beautifyUpdateLog();
});
function beautifyUpdateLog() {
// Get the raw text content
const rawText = document.body.innerText || document.body.textContent;
// Parse the updates
const updates = parseUpdates(rawText);
// Group by month
const groupedUpdates = groupUpdatesByMonth(updates);
// Create the new beautiful interface
createBeautifulInterface(groupedUpdates);
}
function parseUpdates(text) {
const lines = text.split('\n');
const updates = [];
let currentUpdate = null;
const monthNames = ['January', 'February', 'March', 'April', 'May', 'June',
'July', 'August', 'September', 'October', 'November', 'December'];
for (let line of lines) {
line = line.trim();
if (!line) continue;
// Check if it's a date line - now handles ordinal suffixes (1st, 2nd, 3rd, 4th, etc.)
const dateMatch = line.match(/^(January|February|March|April|May|June|July|August|September|October|November|December)\s+(\d{1,2})(?:st|nd|rd|th)?,?\s+(\d{4})(?:\s*\(part\s*(\d+)\))?/i);
if (dateMatch) {
if (currentUpdate && currentUpdate.items.length > 0) {
updates.push(currentUpdate);
}
currentUpdate = {
date: line,
month: dateMatch[1],
day: parseInt(dateMatch[2]),
year: parseInt(dateMatch[3]),
part: dateMatch[4] || '',
items: [],
notes: []
};
} else if (currentUpdate) {
// Check if it's a bullet point
if (line.startsWith('*')) {
let item = line.substring(1).trim();
// Capitalize first letter if it starts with a lowercase letter
if (item.length > 0 && /^[a-z]/.test(item[0])) {
item = item.charAt(0).toUpperCase() + item.slice(1);
}
currentUpdate.items.push(item);
} else if (line.startsWith('-') && !line.startsWith('---')) {
let item = line.substring(1).trim();
// Capitalize first letter if it starts with a lowercase letter
if (item.length > 0 && /^[a-z]/.test(item[0])) {
item = item.charAt(0).toUpperCase() + item.slice(1);
}
currentUpdate.items.push(item);
} else if (line.startsWith('->')) {
// Sub-item - capitalize it too if needed
let subItem = line.substring(2).trim();
if (subItem.length > 0 && /^[a-z]/.test(subItem[0])) {
subItem = subItem.charAt(0).toUpperCase() + subItem.slice(1);
}
if (currentUpdate.items.length > 0) {
currentUpdate.items[currentUpdate.items.length - 1] += '\n → ' + subItem;
}
} else if (line.includes('***') || line.includes('---')) {
// Note separator
continue;
} else {
// Could be a note or continuation
if (line.length > 10 && !line.match(/^(January|February|March|April|May|June|July|August|September|October|November|December)/i)) {
currentUpdate.notes.push(line);
}
}
}
}
// Don't forget the last update
if (currentUpdate && currentUpdate.items.length > 0) {
updates.push(currentUpdate);
}
return updates;
}
function groupUpdatesByMonth(updates) {
const grouped = {};
for (let update of updates) {
const key = `${update.month} ${update.year}`;
if (!grouped[key]) {
grouped[key] = {
month: update.month,
year: update.year,
updates: []
};
}
grouped[key].updates.push(update);
}
// Sort updates within each month by day (descending)
for (let key in grouped) {
grouped[key].updates.sort((a, b) => {
if (a.day === b.day) {
// If same day, sort by part number
const partA = parseInt(a.part) || 0;
const partB = parseInt(b.part) || 0;
return partB - partA;
}
return b.day - a.day;
});
}
return grouped;
}
function createBeautifulInterface(groupedUpdates) {
// Clear the current page
document.body.innerHTML = '';
// Add styles
const style = document.createElement('style');
style.textContent = `
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap');
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
padding: 15px;
line-height: 1.5;
}
.container {
max-width: 1200px;
margin: 0 auto;
animation: fadeIn 0.5s ease-out;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(20px); }
to { opacity: 1; transform: translateY(0); }
}
.header {
background: rgba(255, 255, 255, 0.98);
border-radius: 20px;
padding: 30px;
margin-bottom: 20px;
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
backdrop-filter: blur(10px);
}
.header h1 {
font-size: 2.5em;
font-weight: 700;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
margin-bottom: 5px;
}
.header .subtitle {
color: #6b7280;
font-size: 1.2em;
}
.controls {
background: rgba(255, 255, 255, 0.98);
border-radius: 15px;
padding: 15px;
margin-bottom: 20px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.08);
display: flex;
gap: 15px;
align-items: center;
flex-wrap: wrap;
}
.search-box {
flex: 1;
min-width: 250px;
position: relative;
}
.search-box input {
width: 100%;
padding: 10px 18px 10px 40px;
border: 2px solid #e5e7eb;
border-radius: 8px;
font-size: 0.95em;
transition: all 0.3s ease;
}
.search-box input:focus {
outline: none;
border-color: #667eea;
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
}
.search-box::before {
content: "🔍";
position: absolute;
left: 12px;
top: 50%;
transform: translateY(-50%);
font-size: 1em;
}
.filter-buttons {
display: flex;
gap: 10px;
}
.filter-btn {
padding: 8px 16px;
border: 2px solid #e5e7eb;
background: white;
border-radius: 8px;
cursor: pointer;
transition: all 0.3s ease;
font-weight: 500;
font-size: 0.95em;
}
.filter-btn:hover {
border-color: #667eea;
background: #f9fafb;
}
.filter-btn.active {
background: #667eea;
color: white;
border-color: #667eea;
}
.stats {
display: flex;
gap: 12px;
padding: 8px 16px;
background: #f9fafb;
border-radius: 8px;
}
.stat {
display: flex;
flex-direction: column;
align-items: center;
}
.stat-value {
font-size: 1.3em;
font-weight: 700;
color: #667eea;
}
.stat-label {
font-size: 0.85em;
color: #6b7280;
}
.month-section {
background: rgba(255, 255, 255, 0.98);
border-radius: 15px;
margin-bottom: 15px;
overflow: hidden;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.08);
transition: all 0.3s ease;
}
.month-section:hover {
box-shadow: 0 12px 35px rgba(0, 0, 0, 0.1);
transform: translateY(-1px);
}
.month-header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 15px 25px;
cursor: pointer;
display: flex;
justify-content: space-between;
align-items: center;
user-select: none;
}
.month-header h2 {
font-size: 1.3em;
font-weight: 600;
}
.month-header .toggle-icon {
font-size: 1.5em;
transition: transform 0.3s ease;
transform: rotate(180deg);
}
.month-section.collapsed .toggle-icon {
transform: rotate(0deg);
}
.month-content {
max-height: none;
overflow: hidden;
transition: max-height 0.5s ease;
}
.month-section.collapsed .month-content {
max-height: 0;
}
.update-day {
padding: 15px 25px;
border-bottom: 1px solid #e5e7eb;
}
.update-day:last-child {
border-bottom: none;
}
.update-date {
font-weight: 600;
color: #374151;
margin-bottom: 10px;
font-size: 1em;
display: flex;
align-items: center;
gap: 10px;
}
.update-date .day-badge {
background: #f3f4f6;
padding: 2px 8px;
border-radius: 12px;
font-size: 0.85em;
font-weight: 500;
color: #6b7280;
}
.update-items {
list-style: none;
padding-left: 0;
}
.update-item {
padding: 8px 15px 8px 35px;
margin-bottom: 5px;
background: #f9fafb;
border-radius: 8px;
transition: all 0.3s ease;
position: relative;
list-style: none;
}
.update-item::before {
content: "•";
position: absolute;
left: 15px;
font-size: 1.1em;
font-weight: bold;
color: #9ca3af;
}
.update-item:hover {
background: #f3f4f6;
transform: translateX(3px);
}
.update-item:hover::before {
color: #667eea;
}
.update-item.feature::before {
color: #10b981;
}
.update-item.fix::before {
color: #f59e0b;
}
.update-item.balance::before {
color: #3b82f6;
}
.sub-item {
display: block;
margin-left: 15px;
margin-top: 5px;
padding-left: 10px;
color: #6b7280;
font-size: 0.95em;
}
.update-note {
background: #fef3c7;
border: 1px solid #fcd34d;
border-radius: 8px;
padding: 10px 15px;
margin-top: 10px;
color: #92400e;
font-size: 0.95em;
}
.tag {
display: inline-block;
padding: 1px 6px;
border-radius: 3px;
font-size: 0.8em;
font-weight: 500;
margin-right: 6px;
}
.tag.fix {
background: #fef3c7;
color: #92400e;
}
.tag.balance {
background: #dbeafe;
color: #1e40af;
}
.highlight {
background: #fef08a;
padding: 2px 4px;
border-radius: 3px;
}
.no-results {
text-align: center;
padding: 60px;
color: #6b7280;
font-size: 1.2em;
}
@media (max-width: 768px) {
.header h1 {
font-size: 2em;
}
.controls {
flex-direction: column;
}
.search-box {
width: 100%;
}
.filter-buttons {
width: 100%;
justify-content: center;
}
}
/* Dark mode support */
@media (prefers-color-scheme: dark) {
body {
background: linear-gradient(135deg, #1e293b 0%, #334155 100%);
}
.header, .controls, .month-section {
background: rgba(30, 41, 59, 0.98);
}
.header h1 {
background: linear-gradient(135deg, #818cf8 0%, #c084fc 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.header .subtitle {
color: #94a3b8;
}
.search-box input, .filter-btn {
background: #1e293b;
border-color: #475569;
color: #e2e8f0;
}
.search-box input:focus {
border-color: #818cf8;
box-shadow: 0 0 0 3px rgba(129, 140, 248, 0.2);
}
.filter-btn.active {
background: #818cf8;
}
.stats {
background: #1e293b;
}
.stat-value {
color: #818cf8;
}
.month-header {
background: linear-gradient(135deg, #818cf8 0%, #c084fc 100%);
}
.update-day {
border-bottom-color: #334155;
}
.update-date {
color: #e2e8f0;
}
.update-date .day-badge {
background: #334155;
color: #94a3b8;
}
.update-item {
background: #1e293b;
color: #e2e8f0;
}
.update-item::before {
color: #64748b;
}
.update-item:hover {
background: #334155;
}
.update-item:hover::before {
color: #818cf8;
}
.update-item.feature::before {
color: #10b981;
}
.update-item.fix::before {
color: #f59e0b;
}
.update-item.balance::before {
color: #3b82f6;
}
.sub-item {
color: #94a3b8;
}
.update-note {
background: #422006;
border-color: #92400e;
color: #fef3c7;
}
}
`;
document.head.appendChild(style);
// Create container
const container = document.createElement('div');
container.className = 'container';
// Create header
const header = document.createElement('div');
header.className = 'header';
header.innerHTML = `
<h1>FlatMMO Update Log</h1>
<div class="subtitle">Track all game updates and improvements</div>
`;
container.appendChild(header);
// Create controls
const controls = document.createElement('div');
controls.className = 'controls';
const searchBox = document.createElement('div');
searchBox.className = 'search-box';
searchBox.innerHTML = `
<input type="text" id="searchInput" placeholder="Search updates...">
`;
controls.appendChild(searchBox);
const filterButtons = document.createElement('div');
filterButtons.className = 'filter-buttons';
filterButtons.innerHTML = `
<button class="filter-btn active" data-filter="all">All</button>
<button class="filter-btn" data-filter="feature">Features</button>
<button class="filter-btn" data-filter="fix">Fixes</button>
<button class="filter-btn" data-filter="balance">Balance</button>
`;
controls.appendChild(filterButtons);
// Calculate stats
let totalUpdates = 0;
let totalItems = 0;
for (let key in groupedUpdates) {
totalUpdates += groupedUpdates[key].updates.length;
for (let update of groupedUpdates[key].updates) {
totalItems += update.items.length;
}
}
const stats = document.createElement('div');
stats.className = 'stats';
stats.innerHTML = `
<div class="stat">
<div class="stat-value">${Object.keys(groupedUpdates).length}</div>
<div class="stat-label">Months</div>
</div>
<div class="stat">
<div class="stat-value">${totalUpdates}</div>
<div class="stat-label">Updates</div>
</div>
<div class="stat">
<div class="stat-value">${totalItems}</div>
<div class="stat-label">Changes</div>
</div>
`;
controls.appendChild(stats);
container.appendChild(controls);
// Create update sections
const updatesContainer = document.createElement('div');
updatesContainer.id = 'updatesContainer';
// Sort months (most recent first)
const sortedMonths = Object.keys(groupedUpdates).sort((a, b) => {
const [monthA, yearA] = a.split(' ');
const [monthB, yearB] = b.split(' ');
const monthOrder = ['January', 'February', 'March', 'April', 'May', 'June',
'July', 'August', 'September', 'October', 'November', 'December'];
if (yearA !== yearB) {
return parseInt(yearB) - parseInt(yearA);
}
return monthOrder.indexOf(monthB) - monthOrder.indexOf(monthA);
});
sortedMonths.forEach((monthKey, index) => {
const monthData = groupedUpdates[monthKey];
const monthSection = createMonthSection(monthData, true); // All months expanded by default
updatesContainer.appendChild(monthSection);
});
container.appendChild(updatesContainer);
document.body.appendChild(container);
// Add event listeners
setupEventListeners();
}
function createMonthSection(monthData, expanded = false) {
const section = document.createElement('div');
section.className = `month-section ${expanded ? '' : 'collapsed'}`;
const header = document.createElement('div');
header.className = 'month-header';
header.innerHTML = `
<h2>${monthData.month} ${monthData.year}</h2>
<span class="toggle-icon">⬇</span>
`;
section.appendChild(header);
const content = document.createElement('div');
content.className = 'month-content';
monthData.updates.forEach(update => {
const dayDiv = document.createElement('div');
dayDiv.className = 'update-day';
const dateDiv = document.createElement('div');
dateDiv.className = 'update-date';
dateDiv.innerHTML = `
${update.month} ${update.day}, ${update.year}
${update.part ? `<span class="day-badge">Part ${update.part}</span>` : ''}
`;
dayDiv.appendChild(dateDiv);
const itemsList = document.createElement('ul');
itemsList.className = 'update-items';
update.items.forEach(item => {
const li = document.createElement('li');
li.className = 'update-item';
// Determine item type and add appropriate class
const lowerItem = item.toLowerCase();
if (lowerItem.includes('fix') || lowerItem.includes('bug')) {
li.classList.add('fix');
} else if (lowerItem.includes('balance') || lowerItem.includes('buff') || lowerItem.includes('nerf')) {
li.classList.add('balance');
} else {
li.classList.add('feature');
}
// Add tags for fixes and balance only
let taggedItem = item;
if (lowerItem.includes('fix') && lowerItem.indexOf('fix') < 20) {
taggedItem = `<span class="tag fix">FIX</span> ${taggedItem}`;
} else if (lowerItem.includes('balance:') || lowerItem.startsWith('balance')) {
taggedItem = `<span class="tag balance">BALANCE</span> ${taggedItem}`;
}
// Format sub-items with proper indentation
taggedItem = taggedItem.replace(/\n →/g, '<br><span class="sub-item">→');
taggedItem = taggedItem.replace(/<br><span class="sub-item">→/g, '<br><span class="sub-item">→ ') + (taggedItem.includes('<span class="sub-item">') ? '</span>' : '');
li.innerHTML = taggedItem;
itemsList.appendChild(li);
});
dayDiv.appendChild(itemsList);
// Add notes if any
if (update.notes.length > 0) {
update.notes.forEach(note => {
const noteDiv = document.createElement('div');
noteDiv.className = 'update-note';
noteDiv.textContent = note;
dayDiv.appendChild(noteDiv);
});
}
content.appendChild(dayDiv);
});
section.appendChild(content);
return section;
}
function setupEventListeners() {
// Month section toggle
document.querySelectorAll('.month-header').forEach(header => {
header.addEventListener('click', function() {
const section = this.parentElement;
section.classList.toggle('collapsed');
});
});
// Search functionality
const searchInput = document.getElementById('searchInput');
searchInput.addEventListener('input', function() {
const searchTerm = this.value.toLowerCase();
filterUpdates(searchTerm);
});
// Filter buttons
document.querySelectorAll('.filter-btn').forEach(btn => {
btn.addEventListener('click', function() {
document.querySelectorAll('.filter-btn').forEach(b => b.classList.remove('active'));
this.classList.add('active');
applyFilter(this.dataset.filter);
});
});
// Expand/Collapse all shortcut (Ctrl+E)
document.addEventListener('keydown', function(e) {
if (e.ctrlKey && e.key === 'e') {
e.preventDefault();
const sections = document.querySelectorAll('.month-section');
const anyExpanded = Array.from(sections).some(s => !s.classList.contains('collapsed'));
sections.forEach(section => {
if (anyExpanded) {
section.classList.add('collapsed');
} else {
section.classList.remove('collapsed');
}
});
}
});
}
function filterUpdates(searchTerm) {
const items = document.querySelectorAll('.update-item');
let visibleCount = 0;
items.forEach(item => {
const text = item.textContent.toLowerCase();
if (text.includes(searchTerm)) {
item.style.display = 'block';
visibleCount++;
// Highlight search term
if (searchTerm) {
const regex = new RegExp(`(${searchTerm})`, 'gi');
item.innerHTML = item.innerHTML.replace(/<span class="highlight">([^<]+)<\/span>/g, '$1');
item.innerHTML = item.innerHTML.replace(regex, '<span class="highlight">$1</span>');
}
} else {
item.style.display = 'none';
}
});
// Hide empty sections
document.querySelectorAll('.update-day').forEach(day => {
const visibleItems = day.querySelectorAll('.update-item:not([style*="display: none"])');
day.style.display = visibleItems.length > 0 ? 'block' : 'none';
});
document.querySelectorAll('.month-section').forEach(section => {
const visibleDays = section.querySelectorAll('.update-day:not([style*="display: none"])');
section.style.display = visibleDays.length > 0 ? 'block' : 'none';
});
// Show no results message if needed
const container = document.getElementById('updatesContainer');
const existingMessage = container.querySelector('.no-results');
if (visibleCount === 0 && searchTerm) {
if (!existingMessage) {
const noResults = document.createElement('div');
noResults.className = 'no-results';
noResults.innerHTML = `No updates found matching "${searchTerm}"`;
container.appendChild(noResults);
}
} else if (existingMessage) {
existingMessage.remove();
}
}
function applyFilter(filterType) {
const items = document.querySelectorAll('.update-item');
items.forEach(item => {
if (filterType === 'all') {
item.style.display = 'block';
} else {
item.style.display = item.classList.contains(filterType) ? 'block' : 'none';
}
});
// Hide empty sections
document.querySelectorAll('.update-day').forEach(day => {
const visibleItems = day.querySelectorAll('.update-item:not([style*="display: none"])');
day.style.display = visibleItems.length > 0 ? 'block' : 'none';
});
document.querySelectorAll('.month-section').forEach(section => {
const visibleDays = section.querySelectorAll('.update-day:not([style*="display: none"])');
section.style.display = visibleDays.length > 0 ? 'block' : 'none';
});
}
})();