您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Calculates ranked war targets, API fetching, draggable & minimizable UI (toggle in top-right).
// ==UserScript== // @name Torn Ranked War Calculator // @namespace https://greasyfork.org/en/users/1431907-theeeunknown // @version 1 // @description Calculates ranked war targets, API fetching, draggable & minimizable UI (toggle in top-right). // @author TR0LL // @match https://www.torn.com/factions.php* // @grant GM_addStyle // @grant GM_setValue // @grant GM_getValue // @grant GM_xmlhttpRequest // @connect api.torn.com // @run-at document-idle // @license MIT // ==/UserScript== (function() { 'use strict'; GM_addStyle(` #warCalcContainer { position: fixed; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; border-radius: 10px; padding: 0; margin: 0; box-shadow: 0 6px 24px rgba(0,0,0,0.25); font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; z-index: 999998; width: 360px; max-height: 88vh; display: none; flex-direction: column; top: 80px; right: 10px; /* Default panel position */ } #warCalcHeader { padding: 10px 15px; background: rgba(0,0,0,0.2); border-bottom: 1px solid rgba(255,255,255,0.1); cursor: grab; border-top-left-radius: 10px; border-top-right-radius: 10px; user-select: none; } #warCalcHeader:active { cursor: grabbing; } #warCalcHeader h3 { margin: 0; font-size: 16px; text-align: center; text-shadow: 1px 1px 2px rgba(0,0,0,0.3); } #warCalcContent { padding: 10px 15px 15px 15px; overflow-y: auto; flex-grow: 1; } #warCalcContent::-webkit-scrollbar { width: 6px; } #warCalcContent::-webkit-scrollbar-track { background: rgba(0,0,0,0.1); border-radius: 8px; } #warCalcContent::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.25); border-radius: 8px; } #warCalcContent::-webkit-scrollbar-thumb:hover { background: rgba(255,255,255,0.4); } .war-calc-input-section { background: rgba(255,255,255,0.05); border-radius: 6px; padding: 10px; margin-bottom: 10px; border: 1px solid rgba(255,255,255,0.1); } .war-calc-input-section label { display: block; font-weight: 500; margin-bottom: 5px; font-size: 12px; color: rgba(255,255,255,0.8); } #apiKeyInput, #warAnnounceInput, .input-row input[type="text"], .input-row input[type="number"], #initialTargetScore, #desiredFinalTargetScore { width: 100%; padding: 8px; border: none; border-radius: 4px; background: rgba(0,0,0,0.25); color: #fff; font-size: 12px; box-sizing: border-box; font-family: inherit; } #apiKeyInput { margin-bottom: 8px; } #warAnnounceInput { resize: vertical; min-height: 50px; margin-bottom:10px; } #initialTargetScore, #desiredFinalTargetScore { margin-bottom: 10px; } input[readonly] { background: rgba(0,0,0,0.35) !important; color: #aaa !important; cursor: default !important; } .input-row { display: flex; gap: 10px; margin-bottom: 8px; } .input-row > div { flex: 1; } #fetchApiDataButton, #warCalcButton, #resetWarCalcButton { padding: 8px 12px; margin-top: 8px; font-size: 12px; letter-spacing: 0.3px; color: white; border: none; border-radius: 4px; font-weight: bold; cursor: pointer; width: 100%; transition: all 0.15s ease-in-out; text-transform: uppercase; } #fetchApiDataButton { background: linear-gradient(45deg, #007bff, #0056b3); margin-bottom: 4px; } #fetchApiDataButton:hover { background: linear-gradient(45deg, #0069d9, #004085); transform: translateY(-1px); box-shadow: 0 2px 8px rgba(0,0,0,0.2); } #warCalcButton { background: linear-gradient(45deg, #28a745, #218838); } #warCalcButton:hover { transform: translateY(-1px) scale(1.01); box-shadow: 0 3px 10px rgba(0,0,0,0.2); background: linear-gradient(45deg, #2ebf4e, #259d40); } #resetWarCalcButton { background: linear-gradient(45deg, #dc3545, #c82333); } #resetWarCalcButton:hover { transform: translateY(-1px) scale(1.01); box-shadow: 0 3px 10px rgba(0,0,0,0.2); background: linear-gradient(45deg, #e04b59, #d32f3f); } #apiStatusMessage { font-size: 10px; margin-top: 4px; min-height: 13px;} #warCalcDisplay { background: rgba(0,0,0,0.1); border-radius: 6px; padding: 8px 10px; margin-top: 10px; border: 1px solid rgba(255,255,255,0.1); } .faction-scores { margin-bottom: 10px; padding: 10px 8px; display: flex; justify-content: space-around; align-items: center; background: rgba(255,255,255,0.05); border-radius: 6px; border-bottom: 1px solid rgba(255,255,255,0.1); } .faction-score { text-align: center; flex: 1; padding: 0 4px; } .faction-name { font-size: 11px; margin-bottom: 3px; display: block; opacity: 0.85; word-break: break-word; color: rgba(255,255,255,0.8); font-weight: 500; } .score { font-size: 20px; display: block; font-weight: 700; text-shadow: 1px 1px 2px rgba(0,0,0,0.3); } .vs { font-size: 14px; margin: 0 8px; font-weight: bold; opacity: 0.7; flex-shrink: 0; } .info-row { padding: 7px 5px; font-size: 12px; display: flex; justify-content: space-between; align-items: center; border-bottom: 1px solid rgba(255,255,255,0.08); } .info-row:last-child { border-bottom: none; } .info-row span:first-child { opacity: 0.85; color: rgba(255,255,255,0.8); } .info-row.highlight { margin: 4px -5px; padding: 8px; background: rgba(255,255,255,0.08); border-radius: 4px; font-weight: bold;} .info-row.highlight .info-value { font-size: 13px; } .info-row.success { margin: 4px -5px; padding: 8px; background: rgba(40, 167, 69, 0.2); border-radius: 4px; color: #a0e0a0;} .info-row.success .info-value { font-weight: bold; color: #c0f0c0; } .info-value { font-weight: 600; text-align: right; word-break: break-word; color: #fff; } .status-badge { padding: 3px 8px; font-size: 10px; display: inline-block; border-radius: 12px; font-weight: bold; text-transform: uppercase; letter-spacing: 0.5px; } .status-badge.active { background: #28a745; color: white; } .status-badge.ended { background: #dc3545; color: white; } .status-badge.waiting { background: #ffc107; color: #212529; } .decayed { color: #ffae42 !important; } .error-message { padding: 10px; font-size: 12px; color: #f8d7da; background-color: rgba(220, 53, 69, 0.3); border: 1px solid rgba(220, 53, 69, 0.5); border-radius: 4px; margin-top: 10px; text-align: left;} .status-message { padding: 15px 10px; font-size: 13px; text-align: center; opacity: 0.7; color: rgba(255,255,255,0.7); } /* MODIFIED Toggle Button Styles */ #warCalcToggleButton { position: fixed; top: 20px; /* Changed from bottom */ right: 10px; /* Adjusted for alignment */ background-color: #667eea; color: white; border: none; border-radius: 50%; width: 45px; height: 45px; font-size: 18px; /* For emoji icon */ font-weight: bold; line-height: 45px; /* Vertically center emoji */ text-align: center; /* Horizontally center emoji */ cursor: pointer; box-shadow: 0 2px 8px rgba(0,0,0,0.25); z-index: 1000000; /* Above everything */ transition: background-color 0.2s ease, transform 0.2s ease; } #warCalcToggleButton:hover { background-color: #764ba2; transform: scale(1.05); } `); // --- MAIN PANEL CREATION --- const container = document.createElement('div'); container.id = 'warCalcContainer'; container.innerHTML = ` <div id="warCalcHeader"> <h3>🎯 Ranked War Calculator</h3> </div> <div id="warCalcContent"> <div class="war-calc-input-section"> <label for="apiKeyInput">Torn API Key (Public Faction Access):</label> <input type="password" id="apiKeyInput" placeholder="Enter API Key (optional)"> <button id="fetchApiDataButton">Fetch Active War Data (API)</button> <div id="apiStatusMessage"></div> </div> <div class="war-calc-input-section"> <label for="warAnnounceInput">War Announcement (or API Summary):</label> <textarea id="warAnnounceInput" placeholder="Paste announcement or use API fetch"></textarea> <label for="initialTargetScore">Initial Target Score:</label> <input type="number" id="initialTargetScore" placeholder="e.g., 13200"> <label for="desiredFinalTargetScore">Target Score Diff. (Auto: |A Score - B Score|):</label> <input type="number" id="desiredFinalTargetScore" placeholder="Auto-calculated" readonly> <div class="input-row"> <div><label for="factionAName">Faction A Name:</label><input type="text" id="factionAName" placeholder="Auto-filled" readonly></div> <div><label for="factionAScore">Faction A Score:</label><input type="number" id="factionAScore" placeholder="0"></div> </div> <div class="input-row"> <div><label for="factionBName">Faction B Name:</label><input type="text" id="factionBName" placeholder="Auto-filled" readonly></div> <div><label for="factionBScore">Faction B Score:</label><input type="number" id="factionBScore" placeholder="0"></div> </div> <button id="warCalcButton">Process & Calculate</button> <button id="resetWarCalcButton">Clear & Reset</button> </div> <div id="warCalcDisplay"> <div class="status-message">Enter data or use API fetch, then Process.</div> </div> </div> `; document.body.appendChild(container); // --- TOGGLE BUTTON CREATION & LOGIC --- const toggleButton = document.createElement('button'); toggleButton.id = 'warCalcToggleButton'; toggleButton.innerHTML = '🎯'; // Default: show panel icon document.body.appendChild(toggleButton); let isPanelVisible = GM_getValue('panelVisible', false); function applyPanelVisibility() { if (isPanelVisible) { container.style.display = 'flex'; toggleButton.innerHTML = '🔽'; // Panel is visible, button shows "hide" icon } else { container.style.display = 'none'; toggleButton.innerHTML = '🎯'; // Panel is hidden, button shows "show" icon } } function togglePanel() { isPanelVisible = !isPanelVisible; applyPanelVisibility(); GM_setValue('panelVisible', isPanelVisible); } toggleButton.addEventListener('click', togglePanel); applyPanelVisibility(); // Apply initial state // --- DRAGGABLE LOGIC --- const header = document.getElementById('warCalcHeader'); let isDragging = false; let currentX_drag, currentY_drag, initialX_drag, initialY_drag; const savedLeft = GM_getValue('assistantPosX', null); const savedTop = GM_getValue('assistantPosY', null); if (savedLeft !== null && savedTop !== null) { container.style.left = savedLeft; container.style.top = savedTop; container.style.right = 'auto';} else { /* Default CSS position (top: 80px; right: 10px;) is used if nothing saved */ } header.addEventListener('mousedown', (e) => { if (e.button !== 0) return; if (container.style.right !=='auto' && container.style.right !== '') { container.style.left = container.offsetLeft + 'px'; container.style.right = 'auto'; } initialX_drag = e.clientX - (parseFloat(container.style.left) || 0); initialY_drag = e.clientY - (parseFloat(container.style.top) || 0); isDragging = true; }); document.addEventListener('mousemove', (e) => { if (isDragging) { e.preventDefault(); currentX_drag = e.clientX - initialX_drag; currentY_drag = e.clientY - initialY_drag; container.style.left = currentX_drag + "px"; container.style.top = currentY_drag + "px"; } }); document.addEventListener('mouseup', (e) => { if (e.button !== 0 && isDragging) return; if (isDragging) { GM_setValue('assistantPosX', container.style.left); GM_setValue('assistantPosY', container.style.top); isDragging = false; } }); // --- GET ELEMENTS & 나머지 스크립트 로직 --- // (The rest of the JavaScript: element selections, state variables, helper functions, event listeners, etc. // remains the same as in version 1.3.2 / API version. Ensure all element IDs match.) const apiKeyInputEl = document.getElementById('apiKeyInput'); const fetchApiDataButtonEl = document.getElementById('fetchApiDataButton'); const apiStatusMessageEl = document.getElementById('apiStatusMessage'); const warInfoPasteEl = document.getElementById('warAnnounceInput'); const initialTargetScoreEl = document.getElementById('initialTargetScore'); const desiredFinalTargetScoreEl = document.getElementById('desiredFinalTargetScore'); const factionANameEl = document.getElementById('factionAName'); const factionAScoreEl = document.getElementById('factionAScore'); const factionBNameEl = document.getElementById('factionBName'); const factionBScoreEl = document.getElementById('factionBScore'); const calculateButton = document.getElementById('warCalcButton'); const resetButton = document.getElementById('resetWarCalcButton'); const displayDiv = document.getElementById('warCalcDisplay'); if(desiredFinalTargetScoreEl) desiredFinalTargetScoreEl.readOnly = true; if(factionANameEl) factionANameEl.readOnly = true; if(factionBNameEl) factionBNameEl.readOnly = true; let warStartDate = null; let originalTargetScore = 0; let intervalId = null; if(apiKeyInputEl) apiKeyInputEl.value = GM_getValue('apiKey', ''); if(warInfoPasteEl) warInfoPasteEl.value = GM_getValue('warInfoPaste', ''); if(initialTargetScoreEl) initialTargetScoreEl.value = GM_getValue('initialTargetScore', ''); if(factionANameEl) factionANameEl.value = GM_getValue('factionAName', ''); if(factionAScoreEl) factionAScoreEl.value = GM_getValue('factionAScore', '0'); if(factionBNameEl) factionBNameEl.value = GM_getValue('factionBName', ''); if(factionBScoreEl) factionBScoreEl.value = GM_getValue('factionBScore', '0'); function parseWarInfo(info) { /* ... */ let startDate = null, factionAName = null, factionBName = null; const dateTimeMatch = info.match(/(Sun|Mon|Tue|Wed|Thu|Fri|Sat)\s(\d{2}:\d{2}:\d{2})\s-\s(\d{2})\/(\d{2})\/(\d{2})/); if (dateTimeMatch) { const time = dateTimeMatch[2]; const day = parseInt(dateTimeMatch[3],10); const month = parseInt(dateTimeMatch[4],10)-1; const year = parseInt("20"+dateTimeMatch[5],10); const [h,m,s]=time.split(':').map(Number); startDate = new Date(Date.UTC(year,month,day,h,m,s)); } else { const dtf = info.match(/(\d{2}:\d{2}:\d{2})\s-\s(\d{2})\/(\d{2})\/(\d{2})/); if (dtf) { const t=dtf[1]; const d=parseInt(dtf[2],10); const mo=parseInt(dtf[3],10)-1; const y=parseInt("20"+dtf[4],10); const [h,m,s]=t.split(':').map(Number); startDate = new Date(Date.UTC(y,mo,d,h,m,s)); } } const factionMatch = info.match(/between\s(.*?)\sand\s(.*?)\swill begin/i); if (factionMatch) { factionAName = factionMatch[1].trim(); factionBName = factionMatch[2].trim(); } return { startDate, factionAName, factionBName }; } function formatTimeDifference(ms, showSeconds = true, forNextDrop = false) { /* ... */ if (ms < 0) return forNextDrop ? "Now" : (showSeconds ? "Passed" : "Active"); let totalS = Math.floor(ms/1000); let d=Math.floor(totalS/(24*60*60)); totalS%=(24*60*60); let h=Math.floor(totalS/(60*60)); totalS%=(60*60); let m=Math.floor(totalS/60); let s=totalS%60; let r=[]; if(d>0)r.push(`${d}d`); if(h>0||(d>0&&(m>0||s>0||showSeconds)))r.push(`${h}h`); if(m>0||((h>0||d>0)&&(s>0||showSeconds)))r.push(`${m}m`); if(showSeconds||(d===0&&h===0&&m===0))r.push(`${s}s`); return r.length>0?r.join(' '):(forNextDrop?"Now":(showSeconds?"0s":"Just started")); } function toCustomUTCString(dateObj) { /* ... */ if(!dateObj||!(dateObj instanceof Date)||isNaN(dateObj.getTime()))return"N/A"; const p=(n)=>String(n).padStart(2,'0'); const h=p(dateObj.getUTCHours()),m=p(dateObj.getUTCMinutes()),s=p(dateObj.getUTCSeconds()); const d=p(dateObj.getUTCDate()),M=p(dateObj.getUTCMonth()+1),y=dateObj.getUTCFullYear(); return`${h}:${m}:${s} ${d}/${M}/${y} (UTC)`; } function updateDisplay() { /* ... (same as v1.3.2) ... */ const facAScore = parseInt(factionAScoreEl.value, 10) || 0; const facBScore = parseInt(factionBScoreEl.value, 10) || 0; if(desiredFinalTargetScoreEl) desiredFinalTargetScoreEl.value = Math.abs(facAScore - facBScore); if (!warStartDate || isNaN(warStartDate.getTime()) || !originalTargetScore) { if(displayDiv) displayDiv.innerHTML = '<div class="status-message">Awaiting valid inputs or calculation.</div>'; return; } const now = new Date(); const timeElapsedMs = now.getTime() - warStartDate.getTime(); const actualCombinedScore = facAScore + facBScore; let currentTarget = originalTargetScore; let warStatusClass = "waiting"; let statusText = ""; let timeToDecayText = ""; let totalPercentageDecayApplied = 0; const twentyFourHoursMs = 24*60*60*1000; const decayStartTimestamp = warStartDate.getTime() + twentyFourHoursMs; const timeToDecayMs = decayStartTimestamp - now.getTime(); if (timeElapsedMs < 0) { warStatusClass="waiting"; statusText=`Starts in ${formatTimeDifference(-timeElapsedMs)}`; timeToDecayText="N/A (War not started)"; } else { warStatusClass = timeElapsedMs > 123*60*60*1000 ? "ended" : "active"; statusText = `${formatTimeDifference(timeElapsedMs, false)} elapsed`; if (timeToDecayMs > 0) { timeToDecayText = `Decay starts in ${formatTimeDifference(timeToDecayMs)} (at ${toCustomUTCString(new Date(decayStartTimestamp))})`; } else { const hoursIntoDecayPeriod = Math.floor(-timeToDecayMs/(60*60*1000)); totalPercentageDecayApplied = Math.min(hoursIntoDecayPeriod*1, 99); if (hoursIntoDecayPeriod > 0) currentTarget = Math.max(0, originalTargetScore*(1-(totalPercentageDecayApplied/100))); const msIntoCurrentDecayHourBlock = (-timeToDecayMs)%(60*60*1000); const msUntilNextDrop = (60*60*1000) - msIntoCurrentDecayHourBlock; const timeToNextDropStr = totalPercentageDecayApplied < 99 ? formatTimeDifference(msUntilNextDrop, true, true) : "Max decay"; timeToDecayText = `Active (Since ${toCustomUTCString(new Date(decayStartTimestamp))}). Next drop: ${timeToNextDropStr}. Total: ${totalPercentageDecayApplied}% decayed.`; } } const scoreNeededForCurrentTarget = currentTarget - actualCombinedScore; const approxWarEndTimestamp = warStartDate.getTime() + (123*60*60*1000); let timeToDesiredTargetText = ""; const desiredTargetVal = desiredFinalTargetScoreEl ? parseFloat(desiredFinalTargetScoreEl.value) : 0; if (isNaN(desiredTargetVal)) { timeToDesiredTargetText = "N/A (Scores not set)"; } else if (originalTargetScore > 0 && desiredTargetVal > 0 && desiredTargetVal < originalTargetScore) { const targetReductionNeeded = originalTargetScore - desiredTargetVal; const decayAmountPerHour = originalTargetScore * 0.01; if (decayAmountPerHour > 0) { const hoursOfDecayNeeded = Math.ceil(targetReductionNeeded / decayAmountPerHour); const totalTimeUntilDesiredTargetMs = twentyFourHoursMs + (hoursOfDecayNeeded * 60*60*1000); const desiredTargetReachedTimestamp = warStartDate.getTime() + totalTimeUntilDesiredTargetMs; const timeFromNowToDesiredTargetMs = desiredTargetReachedTimestamp - now.getTime(); if (timeFromNowToDesiredTargetMs < 0) timeToDesiredTargetText = `Would've decayed to ${desiredTargetVal.toLocaleString()} at ${toCustomUTCString(new Date(desiredTargetReachedTimestamp))}`; else timeToDesiredTargetText = `To ${desiredTargetVal.toLocaleString()}: In ${formatTimeDifference(timeFromNowToDesiredTargetMs)} (at ${toCustomUTCString(new Date(desiredTargetReachedTimestamp))})`; } else { timeToDesiredTargetText = "N/A (Initial target won't decay)"; } } else if (desiredTargetVal > 0 && desiredTargetVal >= originalTargetScore) { timeToDesiredTargetText = "N/A (Score diff. is >= Initial Target)"; } else if (desiredTargetVal === 0 && (facAScore !== 0 || facBScore !== 0) ) { timeToDesiredTargetText = "N/A (Scores are equal; diff is 0)"; } else if (desiredTargetVal === 0 && facAScore === 0 && facBScore === 0) { timeToDesiredTargetText = "N/A (Scores are 0)"; } else { timeToDesiredTargetText = "N/A (Invalid score diff. for decay calc)"; } if(displayDiv) displayDiv.innerHTML = `<div class="faction-scores"><div class="faction-score"><span class="faction-name">${factionANameEl.value||"Faction A"}</span><span class="score">${facAScore.toLocaleString()}</span></div><div class="vs">VS</div><div class="faction-score"><span class="faction-name">${factionBNameEl.value||"Faction B"}</span><span class="score">${facBScore.toLocaleString()}</span></div></div><div class="info-row"><span>War Status:</span><span class="info-value"><span class="status-badge ${warStatusClass}">${statusText}</span></span></div><div class="info-row"><span>War Start (UTC):</span><span class="info-value">${toCustomUTCString(warStartDate)}</span></div><div class="info-row"><span>Max End (UTC):</span><span class="info-value">${toCustomUTCString(new Date(approxWarEndTimestamp))}</span></div><div class="info-row"><span>Target Decay:</span><span class="info-value">${timeToDecayText}</span></div><div class="info-row"><span>Decay to Score Diff. (${desiredTargetVal.toLocaleString()}):</span><span class="info-value">${timeToDesiredTargetText}</span></div><div class="info-row"><span>Initial Target:</span><span class="info-value">${originalTargetScore.toLocaleString()}</span></div><div class="info-row highlight"><span>Current Target:</span><span class="info-value ${currentTarget < originalTargetScore && timeElapsedMs >= twentyFourHoursMs ? 'decayed' : ''}">${Math.round(currentTarget).toLocaleString()}</span></div><div class="info-row ${scoreNeededForCurrentTarget <= 0 ? 'success' : ''}"><span>Score Needed (to current target):</span><span class="info-value">${scoreNeededForCurrentTarget <= 0 ? 'TARGET MET! 🎉' : Math.round(scoreNeededForCurrentTarget).toLocaleString()}</span></div>`; } function clearAndReset() { /* ... (same as v1.3.2) ... */ if (intervalId) clearInterval(intervalId); intervalId = null; if(apiKeyInputEl) apiKeyInputEl.value = GM_getValue('apiKey', ''); if(warInfoPasteEl) warInfoPasteEl.value = ''; if(initialTargetScoreEl) initialTargetScoreEl.value = ''; if(desiredFinalTargetScoreEl) desiredFinalTargetScoreEl.value = '0'; if(factionANameEl) factionANameEl.value = ''; if(factionAScoreEl) factionAScoreEl.value = '0'; if(factionBNameEl) factionBNameEl.value = ''; if(factionBScoreEl) factionBScoreEl.value = '0'; warStartDate = null; originalTargetScore = 0; const keysToClear = ['warInfoPaste', 'initialTargetScore', 'factionAName', 'factionAScore', 'factionBName', 'factionBScore']; keysToClear.forEach(key => GM_setValue(key, (key.includes('Score') && !key.includes('initial')) ? '0' : '')); if(displayDiv) displayDiv.innerHTML = '<div class="status-message">Enter data or use API fetch, then Process.</div>'; if(apiStatusMessageEl) apiStatusMessageEl.textContent = 'Enter API Key and Fetch for live data.'; console.log("War Calculator Reset."); } if(resetButton) resetButton.addEventListener('click', clearAndReset); function processAndCalculate() { /* ... (same as v1.3.2) ... */ if (intervalId) clearInterval(intervalId); const pastedInfoFull = warInfoPasteEl ? warInfoPasteEl.value : ''; const parsedPastedInfo = parseWarInfo(pastedInfoFull); if (parsedPastedInfo.startDate) warStartDate = parsedPastedInfo.startDate; originalTargetScore = initialTargetScoreEl ? parseFloat(initialTargetScoreEl.value) : 0; if (parsedPastedInfo.factionAName && factionANameEl && (!factionANameEl.value || factionANameEl.placeholder.includes(factionANameEl.value))) factionANameEl.value = parsedPastedInfo.factionAName; if (parsedPastedInfo.factionBName && factionBNameEl && (!factionBNameEl.value || factionBNameEl.placeholder.includes(factionBNameEl.value))) factionBNameEl.value = parsedPastedInfo.factionBName; if (!warStartDate || isNaN(warStartDate.getTime())) { if(displayDiv) displayDiv.innerHTML = '<div class="error-message">Error: Could not parse/set war start time. Use API or paste announcement.</div>'; return; } if (isNaN(originalTargetScore) || originalTargetScore <= 0) { if(displayDiv) displayDiv.innerHTML = '<div class="error-message">Error: Initial target score must be a positive number.</div>'; return; } if (factionANameEl && factionBNameEl && (!factionANameEl.value || !factionBNameEl.value)) { if(displayDiv) displayDiv.innerHTML = '<div class="error-message">Error: Faction names not set. Use API or paste announcement.</div>'; return; } if(warInfoPasteEl) GM_setValue('warInfoPaste', pastedInfoFull); if(initialTargetScoreEl) GM_setValue('initialTargetScore', initialTargetScoreEl.value); if(factionANameEl) GM_setValue('factionAName', factionANameEl.value); if(factionAScoreEl) GM_setValue('factionAScore', factionAScoreEl.value); if(factionBNameEl) GM_setValue('factionBName', factionBNameEl.value); if(factionBScoreEl) GM_setValue('factionBScore', factionBScoreEl.value); updateDisplay(); intervalId = setInterval(updateDisplay, 1000); } if(calculateButton) calculateButton.addEventListener('click', processAndCalculate); if(fetchApiDataButtonEl) fetchApiDataButtonEl.addEventListener('click', () => { /* ... (API fetch logic same as v1.3.2) ... */ const apiKey = apiKeyInputEl ? apiKeyInputEl.value.trim() : ''; if (!apiKey) { if(apiStatusMessageEl) { apiStatusMessageEl.textContent = 'API Key is required.'; apiStatusMessageEl.style.color = '#F04747';} return; } if(apiStatusMessageEl) { apiStatusMessageEl.textContent = 'Fetching data...'; apiStatusMessageEl.style.color = '#ffae42';} GM_xmlhttpRequest({ method: "GET", url: `https://api.torn.com/faction/?selections=rankedwars&key=${apiKey}&comment=RankedWarCalculatorV1.3.4`, onload: function(response) { try { const data = JSON.parse(response.responseText); if (data.error) { if(apiStatusMessageEl) { apiStatusMessageEl.textContent = `API Error: ${data.error.error} (Code: ${data.error.code})`; apiStatusMessageEl.style.color = '#F04747';} console.error("Torn API Error:", data.error); return; } if (data.rankedwars && Object.keys(data.rankedwars).length > 0) { let activeWarFound = false; for (const warId in data.rankedwars) { const warData = data.rankedwars[warId]; if (warData.war && warData.war.end === 0) { activeWarFound = true; warStartDate = new Date(warData.war.start * 1000); originalTargetScore = warData.war.target; if(initialTargetScoreEl) initialTargetScoreEl.value = originalTargetScore; const factionKeys = Object.keys(warData.factions); if (factionKeys.length === 2) { const fac1Data = warData.factions[factionKeys[0]]; const fac2Data = warData.factions[factionKeys[1]]; if(factionANameEl) factionANameEl.value = fac1Data.name; if(factionAScoreEl) factionAScoreEl.value = fac1Data.score; if(factionBNameEl) factionBNameEl.value = fac2Data.name; if(factionBScoreEl) factionBScoreEl.value = fac2Data.score; } if(warInfoPasteEl) warInfoPasteEl.value = `API: ${factionANameEl.value} vs ${factionBNameEl.value} started ${toCustomUTCString(warStartDate)}. Target: ${originalTargetScore}.`; if(apiStatusMessageEl) { apiStatusMessageEl.textContent = 'Active war data loaded! Processed.'; apiStatusMessageEl.style.color = '#28a745';} GM_setValue('warInfoPaste', warInfoPasteEl.value); GM_setValue('initialTargetScore', initialTargetScoreEl.value); GM_setValue('factionAName', factionANameEl.value); GM_setValue('factionAScore', factionAScoreEl.value); GM_setValue('factionBName', factionBNameEl.value); GM_setValue('factionBScore', factionBScoreEl.value); processAndCalculate(); break; } } if (!activeWarFound && apiStatusMessageEl) { apiStatusMessageEl.textContent = 'No active ranked war found for this faction.'; apiStatusMessageEl.style.color = '#ffae42';} } else { if(apiStatusMessageEl) { apiStatusMessageEl.textContent = 'No ranked war data found.'; apiStatusMessageEl.style.color = '#ffae42';} } } catch (e) { if(apiStatusMessageEl) { apiStatusMessageEl.textContent = 'Failed to parse API response.'; apiStatusMessageEl.style.color = '#F04747';} console.error("Error parsing API response:", e, response.responseText); } }, onerror: function(response) { if(apiStatusMessageEl) { apiStatusMessageEl.textContent = 'API request failed. Check console.'; apiStatusMessageEl.style.color = '#F04747';} console.error("Torn API Request Error:", response); } }); }); if (GM_getValue('warInfoPaste', '') && GM_getValue('initialTargetScore', '')) { setTimeout(() => { const autoPInfo = parseWarInfo(GM_getValue('warInfoPaste', '')); const autoTScore = parseFloat(GM_getValue('initialTargetScore', '')); if (autoPInfo.startDate && !isNaN(autoPInfo.startDate.getTime()) && autoTScore > 0) { processAndCalculate(); } else { if(displayDiv) displayDiv.innerHTML = '<div class="status-message">Loaded data incomplete/invalid. Check inputs & Process.</div>';} }, 250); } if(apiKeyInputEl) apiKeyInputEl.addEventListener('change', () => GM_setValue('apiKey', apiKeyInputEl.value.trim())); [factionAScoreEl, factionBScoreEl].forEach(el => { if(el) el.addEventListener('input', () => { if(desiredFinalTargetScoreEl){ const facASV=parseInt(factionAScoreEl.value,10)||0; const facBSV=parseInt(factionBScoreEl.value,10)||0; desiredFinalTargetScoreEl.value=Math.abs(facASV-facBSV);} if(warStartDate&&!isNaN(warStartDate.getTime())&&originalTargetScore>0){GM_setValue(el.id==='factionAScore'?'factionAScore':'factionBScore',el.value);updateDisplay();}});}); [initialTargetScoreEl, warInfoPasteEl].forEach(el => { if(el) el.addEventListener('change', () => { const kM={'initialTargetScore':'initialTargetScore','warAnnounceInput':'warInfoPaste'}; if(kM[el.id])GM_setValue(kM[el.id],el.value); if(warInfoPasteEl&&initialTargetScoreEl&&warInfoPasteEl.value&&initialTargetScoreEl.value&&parseFloat(initialTargetScoreEl.value)>0)processAndCalculate();});}); if (factionAScoreEl && factionBScoreEl && desiredFinalTargetScoreEl) { const facASV=parseInt(factionAScoreEl.value,10)||0; const facBSV=parseInt(factionBScoreEl.value,10)||0; desiredFinalTargetScoreEl.value=Math.abs(facASV-facBSV);} console.log("Torn Ranked War Calculator (Inspired Design v1.3.4 Minimizable Top-Right) Loaded."); })();