您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Calculates payouts, generates reports, and manages API keys in compliance with Torn's ToS. Supports auto-config for Torn PDA.
// ==UserScript== // @name Torn Chain Payout Calculator // @namespace http://tampermonkey.net/ // @version 1.0 // @description Calculates payouts, generates reports, and manages API keys in compliance with Torn's ToS. Supports auto-config for Torn PDA. // @match https://www.torn.com/war.php?step=chainreport&chainID=* // @match https://www.torn.com/factions.php* // @grant GM_xmlhttpRequest // @grant GM_setValue // @grant GM_getValue // @grant GM_deleteValue // @connect api.torn.com // @license MIT // ==/UserScript== (function() { 'use strict'; // --- PROMISE-BASED GM STORAGE HELPERS --- const GM_get = (key, defaultValue) => new Promise(resolve => resolve(GM_getValue(key, defaultValue))); const GM_set = (key, value) => new Promise(resolve => resolve(GM_setValue(key, value))); const GM_del = (key) => new Promise(resolve => resolve(GM_deleteValue(key))); const API_KEY_STORAGE_KEY = 'chain_payout_api_key'; // --- PDA DETECTION --- const isPDA = typeof window.flutter_inappwebview !== 'undefined' && typeof window.flutter_inappwebview.callHandler === 'function'; // --- PAGE ROUTER --- if (window.location.href.includes('war.php?step=chainreport&chainID=')) { setupChainReportPage(); } else if (window.location.href.includes('factions.php')) { setupFactionPage(); } // ================================================================================= // --- FACTION PAGE LOGIC --- // ================================================================================= function setupFactionPage() { async function markPaidChains() { const newsList = document.querySelector('ul.listWrapper___lJjf7'); if (!newsList) return; const chainLinks = newsList.querySelectorAll('p.message___RSW3S a[href*="chainID="]'); for (const link of chainLinks) { try { const chainID = new URL(link.href, window.location.origin).searchParams.get('chainID'); if (!chainID) continue; const storageKey = `payout_status_${chainID}`; const isPaid = await GM_get(storageKey, false); const messageElement = link.parentElement; if (messageElement) { const existingIndicator = messageElement.querySelector('.paid-status-indicator'); if (isPaid && !existingIndicator) { const paidSpan = document.createElement('span'); paidSpan.textContent = ' [PAID]'; paidSpan.style.color = '#4CAF50'; paidSpan.style.fontWeight = 'bold'; paidSpan.className = 'paid-status-indicator'; messageElement.appendChild(paidSpan); } else if (!isPaid && existingIndicator) { existingIndicator.remove(); } } } catch (e) { console.error("Torn Payout Script: Error processing chain link.", e); } } } function waitForElement(selector) { return new Promise(resolve => { if (document.querySelector(selector)) return resolve(document.querySelector(selector)); const observer = new MutationObserver(() => { if (document.querySelector(selector)) { observer.disconnect(); resolve(document.querySelector(selector)); } }); observer.observe(document.body, { childList: true, subtree: true }); }); } waitForElement('#factions').then(factionsContainer => { const observer = new MutationObserver(() => { clearTimeout(window.factionNewsCheckTimeout); window.factionNewsCheckTimeout = setTimeout(markPaidChains, 250); }); observer.observe(factionsContainer, { childList: true, subtree: true }); markPaidChains(); }); } // ================================================================================= // --- CHAIN REPORT PAGE LOGIC (UPDATED FOR API COMPLIANCE & PDA SUPPORT) --- // ================================================================================= function setupChainReportPage() { const XANAX_COST = 840000; const ATTACKS_PER_XANAX = 10; const urlParams = new URLSearchParams(window.location.search); const chainID = urlParams.get('chainID'); if (!chainID || isNaN(chainID)) return; const style = document.createElement('style'); style.textContent = ` /* Main Payout Container */ #payout-container { background-color: #333; border-radius: 5px; padding: 15px; margin-top: 10px; border: 1px solid #222; box-shadow: 0 1px 3px rgba(0,0,0,0.2); } #payout-container h2 { font-size: 1.1em; color: #ddd; font-weight: bold; padding-bottom: 10px; margin-bottom: 10px; border-bottom: 1px solid #444; margin-top: 0; } #payout-container > p { font-size: 0.9em; color: #bbb; margin: 0 0 15px 0; } #payout-container .payout-table-wrapper { overflow-x: auto; background-color: #2c2c2c; border: 1px solid #444; border-radius: 5px; margin-bottom: 15px; } #payout-container table { border-collapse: collapse; width: 100%; } #payout-container th { background-color: #4CAF50; color: white; padding: 10px; text-align: left; border-bottom: 2px solid #444; white-space: nowrap; } #payout-container td { padding: 8px; text-align: left; border-bottom: 1px solid #333; color: white; white-space: nowrap; cursor: pointer; } #payout-container td.cell-copied { background-color: #007bff !important; color: white !important; transition: background-color 0.1s ease-in-out; } #payout-container tr:nth-child(even) { background-color: #333; } #payout-container tr:hover { background-color: #3d3d3d; } #payout-container .total-row { font-weight: bold; background-color: #4CAF50; color: white; } #payout-container .total-row td { cursor: default; } #payout-container .button-group { display: flex; flex-wrap: wrap; gap: 10px; margin-bottom: 15px; } #payout-container .action-button { padding: 8px 16px; color: white; border: none; border-radius: 5px; cursor: pointer; font-size: 0.9em; } #payout-container .close-button { background-color: #ff4444; } #payout-container .close-button:hover { background-color: #cc0000; } #payout-container .copy-button { background-color: #007bff; } #payout-container .copy-button:hover { background-color: #0056b3; } #payout-container .mark-paid-button { background-color: #ffc107; color: black; } #payout-container .mark-paid-button:hover { background-color: #d39e00; } #payout-container .mark-paid-button:disabled { background-color: #6c757d; cursor: not-allowed; } #payout-container .clickable-name { cursor: pointer; text-decoration: underline; color: #87ceeb; } #payout-container #paid-out-warning { color: #ffc107; font-weight: bold; font-size: 1em; text-align: center; padding: 10px; border: 1px solid #ffc107; border-radius: 5px; margin-bottom: 15px; } #payout-container #individual-report-container { margin-top: 10px; padding: 15px; border: 1px dashed #555; border-radius: 5px; background-color: #252525; } #payout-container #individual-report-container h4 { margin-top: 0; font-size: 1em; } /* API Key Management UI */ #api-key-overlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(0,0,0,0.7); z-index: 9998; display: flex; align-items: center; justify-content: center; } #api-key-container { background-color: #333; border-radius: 5px; padding: 20px; border: 1px solid #222; box-shadow: 0 2px 10px rgba(0,0,0,0.5); width: 90%; max-width: 650px; color: white; } #api-key-container h3 { margin-top: 0; color: #4CAF50; border-bottom: 1px solid #444; padding-bottom: 10px; } #api-key-container p { font-size: 0.9em; color: #ccc; } #api-key-container .tos-table { width: 100%; border-collapse: collapse; font-size: 0.8em; margin: 15px 0; } #api-key-container .tos-table th, #api-key-container .tos-table td { border: 1px solid #555; padding: 6px; text-align: left; } #api-key-container .tos-table th { background-color: #444; } #api-key-container .tos-table code { background: #222; padding: 2px 4px; border-radius: 3px; } #api-key-container input[type="password"] { width: 100%; padding: 8px; border-radius: 3px; border: 1px solid #555; background-color: #222; color: white; margin-bottom: 15px; box-sizing: border-box; } #api-key-container .api-button-group { display: flex; gap: 10px; justify-content: flex-end; } #api-key-container .api-button { padding: 8px 16px; border: none; border-radius: 5px; cursor: pointer; } #api-key-container .save-key { background-color: #4CAF50; color: white; } #api-key-container .clear-key { background-color: #f44336; color: white; } #api-key-container .cancel-key { background-color: #6c757d; color: white; } /* General UI Elements */ .spinner { border: 4px solid rgba(0, 0, 0, 0.1); border-left-color: #4CAF50; border-radius: 50%; width: 24px; height: 24px; animation: spin 1s linear infinite; position: fixed; bottom: 50px; left: 160px; z-index: 1001; } .fixed-button { position: fixed; bottom: 50px; z-index: 1000; color: white; padding: 10px 20px; border: none; border-radius: 5px; cursor: pointer; } @keyframes spin { to { transform: rotate(360deg); } } `; document.head.appendChild(style); const calculateButton = document.createElement('button'); calculateButton.textContent = 'Calculate Payout'; calculateButton.className = 'fixed-button'; calculateButton.style.left = '10px'; calculateButton.style.backgroundColor = '#4CAF50'; document.body.appendChild(calculateButton); const manageApiButton = document.createElement('button'); manageApiButton.textContent = 'Manage API Key'; manageApiButton.className = 'fixed-button'; manageApiButton.style.left = '180px'; manageApiButton.style.backgroundColor = '#007bff'; document.body.appendChild(manageApiButton); const gmFetch = (url) => new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: 'GET', url: url, headers: { 'accept': 'application/json' }, onload: resolve, onerror: reject }); }); async function createApiManagementUI() { const existingUI = document.getElementById('api-key-overlay'); if (existingUI) existingUI.remove(); const overlay = document.createElement('div'); overlay.id = 'api-key-overlay'; const currentKey = await GM_get(API_KEY_STORAGE_KEY, ''); overlay.innerHTML = ` <div id="api-key-container"> <h3>API Key Management</h3> <p>To calculate payouts, this script requires a Torn API key with 'Limited Access'. Your key is stored securely and locally in your browser and is never shared.</p> <table class="tos-table"> <thead><tr><th>Data Storage</th><th>Data Sharing</th><th>Purpose of Use</th><th>Key Storage & Sharing</th><th>Key Access Level</th></tr></thead> <tbody><tr> <td>Only locally</td> <td>Nobody</td> <td>Non-malicious statistical analysis</td> <td>Stored locally / Not shared</td> <td>Limited Access: <code>faction (chainreport, balance)</code></td> </tr></tbody> </table> <input type="password" id="api-key-input" placeholder="Enter your Limited Access API Key" value="${currentKey}"> <div class="api-button-group"> <button class="api-button cancel-key">Cancel</button> <button class="api-button clear-key">Clear Key</button> <button class="api-button save-key">Save Key</button> </div> </div> `; document.body.appendChild(overlay); const apiKeyInput = overlay.querySelector('#api-key-input'); if (isPDA && currentKey === '###PDA-APIKEY###') { apiKeyInput.disabled = true; const pdaNote = document.createElement('p'); pdaNote.style.color = '#87ceeb'; pdaNote.textContent = 'Using Torn PDA managed API key. To change, edit in Torn PDA settings.'; apiKeyInput.insertAdjacentElement('afterend', pdaNote); } const closeUI = () => document.getElementById('api-key-overlay')?.remove(); overlay.querySelector('.save-key').addEventListener('click', async () => { const newKey = overlay.querySelector('#api-key-input').value.trim(); if (newKey) { await GM_set(API_KEY_STORAGE_KEY, newKey); alert('API Key saved!'); closeUI(); } else { alert('API Key cannot be empty. Use the "Clear Key" button to remove it.'); } }); overlay.querySelector('.clear-key').addEventListener('click', async () => { if (confirm('Are you sure you want to clear your stored API key?')) { await GM_del(API_KEY_STORAGE_KEY); alert('API Key cleared.'); closeUI(); } }); overlay.querySelector('.cancel-key').addEventListener('click', closeUI); } manageApiButton.addEventListener('click', createApiManagementUI); calculateButton.addEventListener('click', async () => { let apiKey = await GM_get(API_KEY_STORAGE_KEY, ''); if (!apiKey) { if (isPDA) { await GM_set(API_KEY_STORAGE_KEY, '###PDA-APIKEY###'); apiKey = '###PDA-APIKEY###'; alert('Torn PDA detected. API key has been automatically configured.'); } else { alert('No API key found. Please set your key first.'); await createApiManagementUI(); return; } } const bcrInput = prompt('Enter the Base Chain Reward (BCR) for performance payout (excluding Xanax costs):', '1000000'); const BCR = parseFloat(bcrInput); if (isNaN(BCR) || BCR < 0) { alert('Invalid BCR value.'); return; } const spinner = document.createElement('div'); spinner.className = 'spinner'; document.body.appendChild(spinner); try { const [chainResponse, balanceResponse] = await Promise.all([ gmFetch(`https://api.torn.com/v2/faction/${chainID}/chainreport?selections=&key=${apiKey}`), gmFetch(`https://api.torn.com/v2/faction/balance?key=${apiKey}`) ]); const chainData = JSON.parse(chainResponse.responseText); if (chainData.error) { alert(`API Error (Chain Report): ${chainData.error.error}\n\nThis may be an invalid key or insufficient permissions. Click 'Manage API Key' to check.`); return; } const balanceData = JSON.parse(balanceResponse.responseText); if (balanceData.error) { alert(`API Error (Faction Balance): ${balanceData.error.error}`); return; } await calculateAndDisplayPayouts(chainData.chainreport, balanceData, BCR); } catch (error) { console.error('Torn Payout Script Error:', error); alert('A network error occurred. Check your API key and internet connection.'); } finally { if (spinner && document.body.contains(spinner)) document.body.removeChild(spinner); } }); async function calculateAndDisplayPayouts(chainReport, balanceData, BCR) { const attackers = chainReport.attackers; let totalICS = 0; const memberDataMap = new Map(); if (balanceData?.balance?.members) { balanceData.balance.members.forEach(member => { memberDataMap.set(member.id, { username: member.username, money: member.money }); }); } attackers.forEach(attacker => { attacker.ICS = (attacker.attacks.total * 5) + (attacker.respect.total * 100); totalICS += attacker.ICS; const memberInfo = memberDataMap.get(attacker.id); attacker.username = memberInfo?.username || `User #${attacker.id}`; attacker.currentBalance = memberInfo?.money ?? null; }); let totalFinalPayout = 0; attackers.forEach(attacker => { attacker.performancePayout = totalICS > 0 ? (BCR * (attacker.ICS / totalICS)) : 0; attacker.xanaxReimbursement = (ATTACKS_PER_XANAX > 0 && XANAX_COST > 0) ? (attacker.attacks.total / ATTACKS_PER_XANAX) * XANAX_COST : 0; attacker.payout = attacker.performancePayout + attacker.xanaxReimbursement; attacker.newBalance = attacker.currentBalance !== null ? attacker.currentBalance + attacker.payout : null; totalFinalPayout += attacker.payout; }); const existingContainer = document.getElementById('payout-container'); if (existingContainer) existingContainer.remove(); const payoutContainer = document.createElement('div'); payoutContainer.id = 'payout-container'; const reportWrapper = document.querySelector('div.chain-report-wrap.chainReportWp___G8B3E'); if (reportWrapper) { reportWrapper.appendChild(payoutContainer); } else { const mainContainer = document.getElementById('mainContainer') || document.body; mainContainer.appendChild(payoutContainer); } const storageKey = `payout_status_${chainID}`; let isPaid = await GM_get(storageKey, false); payoutContainer.insertAdjacentHTML('beforeend', ` <div id="paid-out-warning" style="display: ${isPaid ? 'block' : 'none'};"><strong>This chain has already been marked as paid out.</strong></div> <h2>Chain Payout Calculator</h2> <p>Base Chain Reward (BCR): ${formatCurrency(BCR)} | Xanax Reimbursement: ${formatCurrency(XANAX_COST)} per ${ATTACKS_PER_XANAX} attacks.</p> `); const tableWrapper = document.createElement('div'); tableWrapper.className = 'payout-table-wrapper'; const table = document.createElement('table'); table.innerHTML = `<thead><tr> <th>Player Name [ID]</th><th>Respect</th><th>Attacks</th><th>Final Payout</th><th>Current Balance</th><th>New Balance</th> </tr></thead>`; const tbody = document.createElement('tbody'); attackers.forEach(attacker => { tbody.innerHTML += ` <tr> <td class="clickable-name" data-id="${attacker.id}">${attacker.username} [${attacker.id}]</td> <td>${attacker.respect.total.toFixed(2)}</td> <td>${attacker.attacks.total}</td> <td>${formatCurrency(attacker.payout)}</td> <td>${attacker.currentBalance !== null ? formatCurrency(attacker.currentBalance) : 'N/A'}</td> <td>${attacker.newBalance !== null ? formatCurrency(attacker.newBalance) : 'N/A'}</td> </tr> `; }); table.appendChild(tbody); table.insertAdjacentHTML('beforeend', ` <tfoot><tr class="total-row"> <td colspan="3"><strong>Total Final Payout</strong></td> <td><strong>${formatCurrency(totalFinalPayout)}</strong></td> <td colspan="2"></td> </tr></tfoot> `); tableWrapper.appendChild(table); payoutContainer.appendChild(tableWrapper); const buttonGroup = document.createElement('div'); buttonGroup.className = 'button-group'; payoutContainer.appendChild(buttonGroup); const copyBalancesButton = createButton('Copy Balances', 'action-button copy-button', () => { const text = attackers.filter(a => a.newBalance !== null).map(a => `${a.username} [${a.id}] New Balance: ${formatCurrency(a.newBalance)}`).join('\n'); copyToClipboard(text, copyBalancesButton, 'Copy Balances'); }); const copyFactionEmailButton = createButton('Copy Faction Email', 'action-button copy-button', () => { const factionReportHTML = generateFactionWideReportHTML(attackers, chainReport, BCR, totalFinalPayout); copyToClipboard(factionReportHTML.replace(/\n\s*/g, ' ').trim(), copyFactionEmailButton, 'Copy Faction Email'); }); copyFactionEmailButton.style.backgroundColor = '#28a745'; const markPaidButton = createButton('Mark as Paid', 'action-button mark-paid-button', async () => { await GM_set(storageKey, true); document.getElementById('paid-out-warning').style.display = 'block'; markPaidButton.disabled = true; markPaidButton.textContent = 'Paid!'; }); markPaidButton.disabled = isPaid; const closeButton = createButton('Close', 'action-button close-button', () => payoutContainer.remove()); buttonGroup.append(copyBalancesButton, copyFactionEmailButton, markPaidButton, closeButton); const individualReportContainer = document.createElement('div'); individualReportContainer.id = 'individual-report-container'; individualReportContainer.style.display = 'none'; payoutContainer.appendChild(individualReportContainer); table.addEventListener('click', (event) => { const target = event.target; if (target.classList.contains('clickable-name')) { const attackerId = parseInt(target.dataset.id, 10); const attacker = attackers.find(a => a.id === attackerId); generateIndividualReport(attacker, individualReportContainer, chainReport); } else { handleTableCellClick(event); } }); payoutContainer.scrollIntoView({ behavior: 'smooth' }); } function handleTableCellClick(event) { const cell = event.target.closest('td'); if (!cell || cell.closest('thead') || cell.closest('tfoot')) return; let valueToCopy = cell.textContent.trim(); if (!valueToCopy || valueToCopy === 'N/A') return; if (valueToCopy.startsWith('$')) { valueToCopy = valueToCopy.replace(/[$,]/g, ''); } navigator.clipboard.writeText(valueToCopy).then(() => { cell.classList.add('cell-copied'); setTimeout(() => cell.classList.remove('cell-copied'), 1500); }).catch(err => { console.error('Torn Payout Script: Could not copy cell value.', err); }); } function generateIndividualReport(attacker, container, chainReport) { container.style.display = 'block'; container.innerHTML = `<h4>HTML Payout Report for ${attacker.username} [${attacker.id}]</h4>`; const isNewBalanceRowPresent = attacker.newBalance !== null; const tdBorderStyle = "1px solid #444"; const tdNoBorderStyle = "none"; const xanaxRowBottomBorder = isNewBalanceRowPresent ? tdBorderStyle : tdNoBorderStyle; const thStyleLeft = "background-color: #4CAF50; color: white; padding: 12px 15px; text-align: left; border-bottom: 1px solid #3c8e40; font-size: 1em;"; const thStyleRight = "background-color: #4CAF50; color: white; padding: 12px 15px; text-align: right; border-bottom: 1px solid #3c8e40; font-size: 1em;"; const tdStyleLeft = "padding: 10px 15px; text-align: left; color: #e0e0e0;"; const tdStyleRight = "padding: 10px 15px; text-align: right; color: #e0e0e0;"; const totalPayoutColor = "#76ff03"; const reportHTML = `<div style="font-family: Arial, Helvetica, sans-serif; background-color: #1a1a1a; color: #e0e0e0; padding: 20px; max-width: 600px; margin: 0 auto; border-radius: 8px; border: 1px solid #333;"><h3 style="color: #e0e0e0; border-bottom: 2px solid #4CAF50; padding-bottom: 10px; margin-top: 0; margin-bottom: 20px; font-size: 1.4em; text-align: center;">Chain Payout Report</h3><p style="color: #e0e0e0; font-size: 1.1em; margin-bottom: 10px; line-height: 1.6;">Hi ${attacker.username},</p><p style="color: #cccccc; line-height: 1.6; margin-bottom: 15px;">Thank you for your contribution to chain ID: ${chainReport.id}. Here's your payout summary:</p><table style="width: 100%; border-collapse: collapse; background-color: #2c2c2c; color: #e0e0e0; margin-bottom: 20px; border: 1px solid #444; border-radius: 5px; overflow: hidden;"><thead><tr><th style="${thStyleLeft}">Description</th><th style="${thStyleRight}">Value</th></tr></thead><tbody><tr style="background-color: #2c2c2c;"><td style="${tdStyleLeft} border-bottom: ${tdBorderStyle};">Total Attacks</td><td style="${tdStyleRight} border-bottom: ${tdBorderStyle};">${attacker.attacks.total}</td></tr><tr style="background-color: #333333;"><td style="${tdStyleLeft} border-bottom: ${tdBorderStyle};">Total Respect Gained</td><td style="${tdStyleRight} border-bottom: ${tdBorderStyle};">${attacker.respect.total.toFixed(2)}</td></tr><tr style="background-color: #2c2c2c;"><td style="${tdStyleLeft} border-bottom: ${tdBorderStyle};">Performance Payout (ICS)</td><td style="${tdStyleRight} border-bottom: ${tdBorderStyle};">${formatCurrency(attacker.performancePayout)}</td></tr><tr style="background-color: #333333;"><td style="${tdStyleLeft} border-bottom: ${tdBorderStyle};">Xanax Reimbursement</td><td style="${tdStyleRight} border-bottom: ${tdBorderStyle};">${formatCurrency(attacker.xanaxReimbursement)}</td></tr><tr style="background-color: #2c2c2c; font-weight: bold;"><td style="${tdStyleLeft} border-bottom: ${xanaxRowBottomBorder};">Total Payout</td><td style="${tdStyleRight} border-bottom: ${xanaxRowBottomBorder}; color: ${totalPayoutColor};">${formatCurrency(attacker.payout)}</td></tr>${isNewBalanceRowPresent ? `<tr style="background-color: #333333;"><td style="${tdStyleLeft} border-bottom: ${tdNoBorderStyle};">New Faction Balance</td><td style="${tdStyleRight} border-bottom: ${tdNoBorderStyle};">${formatCurrency(attacker.newBalance)}</td></tr>` : ''}</tbody></table><p style="color: #cccccc; line-height: 1.6; margin-bottom: 15px;">The funds have been ${isNewBalanceRowPresent ? 'added to your faction balance' : 'sent to you'}. Keep up the great work!</p><p style="font-size: 0.85em; color: #999999; text-align: center; margin-top: 30px; margin-bottom: 0;"><em>This is an automated message generated by the Faction Payout Script.</em></p></div>`; container.insertAdjacentHTML('beforeend', reportHTML); const copyHtmlButton = createButton('Copy HTML for Mail', 'action-button copy-button', () => { copyToClipboard(reportHTML.replace(/\n\s*/g, ' ').trim(), copyHtmlButton, 'Copy HTML for Mail'); }); copyHtmlButton.style.marginTop = '10px'; container.appendChild(copyHtmlButton); container.scrollIntoView({ behavior: 'smooth', block: 'end' }); } function generateFactionWideReportHTML(attackers, chainReport, bcr, totalChainFinalPayout) { const thStyleLeft = "background-color: #4CAF50; color: white; padding: 10px 12px; text-align: left; border-bottom: 1px solid #3c8e40; font-size: 0.95em;"; const thStyleRight = "background-color: #4CAF50; color: white; padding: 10px 12px; text-align: right; border-bottom: 1px solid #3c8e40; font-size: 0.95em;"; const tdStyleLeft = "padding: 8px 12px; text-align: left; color: #e0e0e0; border-bottom: 1px solid #444;"; const tdStyleRight = "padding: 8px 12px; text-align: right; color: #e0e0e0; border-bottom: 1px solid #444;"; const lastDataRowTdStyleLeft = "padding: 8px 12px; text-align: left; color: #e0e0e0; border-bottom: none;"; const lastDataRowTdStyleRight = "padding: 8px 12px; text-align: right; color: #e0e0e0; border-bottom: none;"; let attackersHtml = ''; const numAttackers = attackers.length; if (numAttackers === 0) { attackersHtml = `<tr><td colspan="4" style="${tdStyleLeft} text-align: center; border-bottom: none;">No participants in this chain.</td></tr>`; } else { attackers.forEach((attacker, index) => { const rowBackground = index % 2 === 0 ? '#2c2c2c' : '#333333'; attackersHtml += `<tr style="background-color: ${rowBackground};"><td style="${index === numAttackers - 1 ? lastDataRowTdStyleLeft : tdStyleLeft}">${attacker.username} [${attacker.id}]</td><td style="${index === numAttackers - 1 ? lastDataRowTdStyleRight : tdStyleRight}">${attacker.respect.total.toFixed(2)}</td><td style="${index === numAttackers - 1 ? lastDataRowTdStyleRight : tdStyleRight}">${attacker.attacks.total}</td><td style="${index === numAttackers - 1 ? lastDataRowTdStyleRight : tdStyleRight}; color: #76ff03;">${formatCurrency(attacker.payout)}</td></tr>`; }); } const reportHTML = `<div style="font-family: Arial, Helvetica, sans-serif; background-color: #1a1a1a; color: #e0e0e0; padding: 20px; max-width: 700px; margin: 0 auto; border-radius: 8px; border: 1px solid #333;"><h3 style="color: #e0e0e0; border-bottom: 2px solid #4CAF50; padding-bottom: 10px; margin-top: 0; margin-bottom: 20px; font-size: 1.4em; text-align: center;">Faction Chain Payout Summary</h3><p style="color: #cccccc; line-height: 1.6; margin-bottom: 15px;">Here's a summary of the payouts for our recent chain (ID: ${chainReport.id}), based on a BCR of ${formatCurrency(bcr)} and Xanax reimbursement of ${formatCurrency(XANAX_COST)} per ${ATTACKS_PER_XANAX} attacks.</p><table style="width: 100%; border-collapse: collapse; background-color: #2c2c2c; color: #e0e0e0; margin-bottom: 20px; border: 1px solid #444; border-radius: 5px; overflow: hidden;"><thead><tr><th style="${thStyleLeft}">Player Name [ID]</th><th style="${thStyleRight}">Respect</th><th style="${thStyleRight}">Attacks</th><th style="${thStyleRight}">Final Payout</th></tr></thead><tbody>${attackersHtml}${(totalChainFinalPayout > 0 && numAttackers > 0) ? `<tr style="background-color: #4CAF50; color: white; font-weight: bold;"><td colspan="3" style="padding: 10px 12px; text-align: left; border-top: 1px solid #3c8e40;"><strong>Total Payout Distributed</strong></td><td style="padding: 10px 12px; text-align: right; border-top: 1px solid #3c8e40;"><strong>${formatCurrency(totalChainFinalPayout)}</strong></td></tr>` : ''}</tbody></table><p style="color: #cccccc; line-height: 1.6;">Well done to all participants!</p><p style="font-size: 0.85em; color: #999999; text-align: center; margin-top: 30px; margin-bottom: 0;"><em>This is an automated message generated by the Faction Payout Script.</em></p></div>`; return reportHTML; } const formatCurrency = (num) => '$' + Math.round(num).toString().replace(/\B(?=(\d{3})+(?!\d))/g, ','); const createButton = (text, className, onClick) => { const btn = document.createElement('button'); btn.textContent = text; btn.className = className; btn.addEventListener('click', onClick); return btn; }; const copyToClipboard = (text, button, originalText) => { navigator.clipboard.writeText(text).then(() => { button.textContent = 'Copied!'; setTimeout(() => { button.textContent = originalText; }, 2000); }).catch(err => { console.error('Failed to copy:', err); alert('Could not copy to clipboard.'); }); }; } })();