您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Makes FlatMMO update log beautiful and organized by month with clean formatting
// ==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'; }); } })();